mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Merge remote-tracking branch 'fork/floodgate-2.0' into floodgate-2.0
# Conflicts: # common/src/main/java/org/geysermc/floodgate/util/BedrockData.java # common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java # connector/src/main/java/org/geysermc/connector/GeyserConnector.java # connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
This commit is contained in:
commit
bb20b14e4c
10 changed files with 447 additions and 146 deletions
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* 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/Floodgate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.crypto;
|
||||||
|
|
||||||
|
import org.geysermc.floodgate.util.InvalidHeaderException;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public final class AesCipher implements FloodgateCipher {
|
||||||
|
private static final int IV_LENGTH = 12;
|
||||||
|
private static final int TAG_BIT_LENGTH = 128;
|
||||||
|
private static final String CIPHER_NAME = "AES/GCM/NoPadding";
|
||||||
|
|
||||||
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
private SecretKey secretKey;
|
||||||
|
|
||||||
|
public void init(Key key) {
|
||||||
|
if (!"AES".equals(key.getAlgorithm())) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Algorithm was expected to be AES, but got " + key.getAlgorithm()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
secretKey = (SecretKey) key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encrypt(byte[] data) throws Exception {
|
||||||
|
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
|
||||||
|
|
||||||
|
byte[] iv = new byte[IV_LENGTH];
|
||||||
|
secureRandom.nextBytes(iv);
|
||||||
|
|
||||||
|
GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
|
||||||
|
byte[] cipherText = cipher.doFinal(data);
|
||||||
|
|
||||||
|
return ByteBuffer.allocate(iv.length + cipherText.length + HEADER_LENGTH)
|
||||||
|
.put(IDENTIFIER).put(VERSION) // header
|
||||||
|
.put(iv)
|
||||||
|
.put(cipherText)
|
||||||
|
.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] decrypt(byte[] cipherTextWithIv) throws Exception {
|
||||||
|
HeaderResult pair = checkHeader(cipherTextWithIv);
|
||||||
|
if (pair.getVersion() != VERSION) {
|
||||||
|
throw new InvalidHeaderException(
|
||||||
|
"Expected version " + VERSION + ", got " + pair.getVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
|
||||||
|
|
||||||
|
int bufferLength = cipherTextWithIv.length - HEADER_LENGTH;
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(cipherTextWithIv, HEADER_LENGTH, bufferLength);
|
||||||
|
|
||||||
|
byte[] iv = new byte[IV_LENGTH];
|
||||||
|
buffer.get(iv);
|
||||||
|
|
||||||
|
byte[] cipherText = new byte[buffer.remaining()];
|
||||||
|
buffer.get(cipherText);
|
||||||
|
|
||||||
|
GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
|
||||||
|
return cipher.doFinal(cipherText);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* 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/Floodgate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.crypto;
|
||||||
|
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public final class AesKeyProducer implements KeyProducer {
|
||||||
|
public static int KEY_SIZE = 128;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SecretKey produce() {
|
||||||
|
try {
|
||||||
|
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
|
||||||
|
keyGenerator.init(KEY_SIZE, SecureRandom.getInstanceStrong());
|
||||||
|
return keyGenerator.generateKey();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SecretKey produceFrom(byte[] keyFileData) {
|
||||||
|
try {
|
||||||
|
return new SecretKeySpec(keyFileData, "AES");
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* 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/Floodgate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.crypto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.geysermc.floodgate.util.InvalidHeaderException;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.Key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for both encrypting and decrypting data
|
||||||
|
*/
|
||||||
|
public interface FloodgateCipher {
|
||||||
|
byte[] IDENTIFIER = "Floodgate".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte VERSION = 2;
|
||||||
|
|
||||||
|
int HEADER_LENGTH = IDENTIFIER.length + 1; // one byte for version
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the instance by giving it the key it needs to encrypt or decrypt data
|
||||||
|
*
|
||||||
|
* @param key the key used to encrypt and decrypt data
|
||||||
|
*/
|
||||||
|
void init(Key key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the given data using the Key provided in {@link #init(Key)}
|
||||||
|
*
|
||||||
|
* @param data the data to encrypt
|
||||||
|
* @return the encrypted data
|
||||||
|
* @throws Exception when the encryption failed
|
||||||
|
*/
|
||||||
|
byte[] encrypt(byte[] data) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts data from a String.<br>
|
||||||
|
* This method internally calls {@link #encrypt(byte[])}
|
||||||
|
*
|
||||||
|
* @param data the data to encrypt
|
||||||
|
* @return the encrypted data
|
||||||
|
* @throws Exception when the encryption failed
|
||||||
|
*/
|
||||||
|
default byte[] encryptFromString(String data) throws Exception {
|
||||||
|
return encrypt(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the given data using the Key provided in {@link #init(Key)}
|
||||||
|
*
|
||||||
|
* @param data the data to decrypt
|
||||||
|
* @return the decrypted data
|
||||||
|
* @throws Exception when the decrypting failed
|
||||||
|
*/
|
||||||
|
byte[] decrypt(byte[] data) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a byte[] and turn it into a String.<br>
|
||||||
|
* This method internally calls {@link #decrypt(byte[])}
|
||||||
|
* and converts the returned byte[] into a String.
|
||||||
|
*
|
||||||
|
* @param data the data to encrypt
|
||||||
|
* @return the decrypted data in a UTF-8 String
|
||||||
|
* @throws Exception when the decrypting failed
|
||||||
|
*/
|
||||||
|
default String decryptToString(byte[] data) throws Exception {
|
||||||
|
byte[] decrypted = decrypt(data);
|
||||||
|
if (decrypted == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new String(decrypted, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a String.<br>
|
||||||
|
* This method internally calls {@link #decrypt(byte[])}
|
||||||
|
* by converting the UTF-8 String into a byte[]
|
||||||
|
*
|
||||||
|
* @param data the data to decrypt
|
||||||
|
* @return the decrypted data in a byte[]
|
||||||
|
* @throws Exception when the decrypting failed
|
||||||
|
*/
|
||||||
|
default byte[] decryptFromString(String data) throws Exception {
|
||||||
|
return decrypt(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the header is valid and return a IntPair containing the header version
|
||||||
|
* and the index to start reading the actual encrypted data from.
|
||||||
|
*
|
||||||
|
* @param data the data to check
|
||||||
|
* @return IntPair. x = version number, y = the index to start reading from.
|
||||||
|
* @throws InvalidHeaderException when the header is invalid
|
||||||
|
*/
|
||||||
|
default HeaderResult checkHeader(byte[] data) throws InvalidHeaderException {
|
||||||
|
final int identifierLength = IDENTIFIER.length;
|
||||||
|
|
||||||
|
if (data.length <= HEADER_LENGTH) {
|
||||||
|
throw new InvalidHeaderException("Data length is smaller then header." +
|
||||||
|
"Needed " + HEADER_LENGTH + ", got " + data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < identifierLength; i++) {
|
||||||
|
if (IDENTIFIER[i] != data[i]) {
|
||||||
|
StringBuilder receivedIdentifier = new StringBuilder();
|
||||||
|
for (byte b : IDENTIFIER) {
|
||||||
|
receivedIdentifier.append(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidHeaderException(String.format(
|
||||||
|
"Expected identifier %s, got %s",
|
||||||
|
new String(IDENTIFIER, StandardCharsets.UTF_8),
|
||||||
|
receivedIdentifier.toString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HeaderResult(data[identifierLength], HEADER_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean hasHeader(String data) {
|
||||||
|
if (data.length() < IDENTIFIER.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < IDENTIFIER.length; i++) {
|
||||||
|
if (IDENTIFIER[i] != data.charAt(i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
class HeaderResult {
|
||||||
|
private int version;
|
||||||
|
private int startIndex;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* 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/Floodgate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.crypto;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.Key;
|
||||||
|
|
||||||
|
public interface KeyProducer {
|
||||||
|
Key produce();
|
||||||
|
Key produceFrom(byte[] keyFileData);
|
||||||
|
|
||||||
|
default Key produceFrom(Path keyFileLocation) throws IOException {
|
||||||
|
return produceFrom(Files.readAllBytes(keyFileLocation));
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,11 +34,10 @@ import lombok.Getter;
|
||||||
* This class is only used internally, and you should look at FloodgatePlayer instead
|
* This class is only used internally, and you should look at FloodgatePlayer instead
|
||||||
* (FloodgatePlayer is present in the common module in the Floodgate repo)
|
* (FloodgatePlayer is present in the common module in the Floodgate repo)
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
@AllArgsConstructor(access = AccessLevel.PACKAGE)
|
||||||
@Getter
|
@Getter
|
||||||
public final class BedrockData {
|
public final class BedrockData {
|
||||||
public static final int EXPECTED_LENGTH = 9;
|
public static final int EXPECTED_LENGTH = 9;
|
||||||
public static final String FLOODGATE_IDENTIFIER = "Geyser-Floodgate";
|
|
||||||
|
|
||||||
private final String version;
|
private final String version;
|
||||||
private final String username;
|
private final String username;
|
||||||
|
@ -65,9 +64,15 @@ public final class BedrockData {
|
||||||
this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, skin);
|
this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, skin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasPlayerLink() {
|
||||||
|
return linkedPlayer != null;
|
||||||
|
}
|
||||||
|
|
||||||
public static BedrockData fromString(String data, String skin) {
|
public static BedrockData fromString(String data, String skin) {
|
||||||
String[] split = data.split("\0");
|
String[] split = data.split("\0");
|
||||||
if (split.length != EXPECTED_LENGTH) return emptyData(split.length);
|
if (split.length != EXPECTED_LENGTH) {
|
||||||
|
return emptyData(split.length);
|
||||||
|
}
|
||||||
|
|
||||||
LinkedPlayer linkedPlayer = LinkedPlayer.fromString(split[8]);
|
LinkedPlayer linkedPlayer = LinkedPlayer.fromString(split[8]);
|
||||||
// The format is the same as the order of the fields in this class
|
// The format is the same as the order of the fields in this class
|
||||||
|
@ -90,10 +95,6 @@ public final class BedrockData {
|
||||||
(linkedPlayer != null ? linkedPlayer.toString() : "null");
|
(linkedPlayer != null ? linkedPlayer.toString() : "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasPlayerLink() {
|
|
||||||
return linkedPlayer != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BedrockData emptyData(int dataLength) {
|
private static BedrockData emptyData(int dataLength) {
|
||||||
return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength, null);
|
return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.geysermc.floodgate.util;
|
|
||||||
|
|
||||||
import javax.crypto.*;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.*;
|
|
||||||
import java.security.spec.EncodedKeySpec;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The class which contains all the encryption and decryption method used in Geyser and Floodgate
|
|
||||||
* (for Floodgate data). This is only used internally and doesn't serve a purpose for anything else
|
|
||||||
*/
|
|
||||||
public final class EncryptionUtil {
|
|
||||||
public static String encrypt(Key key, String data) throws IllegalBlockSizeException,
|
|
||||||
InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
|
|
||||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
|
||||||
generator.init(128);
|
|
||||||
SecretKey secretKey = generator.generateKey();
|
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance("AES");
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
|
||||||
byte[] encryptedText = cipher.doFinal(data.getBytes());
|
|
||||||
|
|
||||||
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
|
||||||
cipher.init(key instanceof PublicKey ? Cipher.PUBLIC_KEY : Cipher.PRIVATE_KEY, key);
|
|
||||||
return Base64.getEncoder().encodeToString(cipher.doFinal(secretKey.getEncoded())) + '\0' +
|
|
||||||
Base64.getEncoder().encodeToString(encryptedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String encryptBedrockData(Key key, BedrockData data, boolean includeSkin) throws IllegalBlockSizeException,
|
|
||||||
InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
|
|
||||||
return encrypt(key, data.toString()) + (includeSkin ? data.getSkin() : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String encryptBedrockData(Key key, BedrockData data) throws IllegalBlockSizeException,
|
|
||||||
InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
|
|
||||||
return encryptBedrockData(key, data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] decrypt(Key key, String encryptedData) throws IllegalBlockSizeException,
|
|
||||||
InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
|
|
||||||
String[] split = encryptedData.split("\0");
|
|
||||||
if (split.length != 2) {
|
|
||||||
throw new IllegalArgumentException("Expected two arguments, got " + split.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
|
||||||
cipher.init(key instanceof PublicKey ? Cipher.PUBLIC_KEY : Cipher.PRIVATE_KEY, key);
|
|
||||||
byte[] decryptedKey = cipher.doFinal(Base64.getDecoder().decode(split[0]));
|
|
||||||
|
|
||||||
SecretKey secretKey = new SecretKeySpec(decryptedKey, 0, decryptedKey.length, "AES");
|
|
||||||
cipher = Cipher.getInstance("AES");
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
|
||||||
return cipher.doFinal(Base64.getDecoder().decode(split[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BedrockData decryptBedrockData(Key key, String encryptedData, String skin) throws IllegalBlockSizeException,
|
|
||||||
InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
|
|
||||||
return BedrockData.fromRawData(decrypt(key, encryptedData), skin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T extends Key> T getKeyFromFile(Path fileLocation, Class<T> keyType) throws
|
|
||||||
IOException, InvalidKeySpecException, NoSuchAlgorithmException {
|
|
||||||
boolean isPublicKey = keyType == PublicKey.class;
|
|
||||||
if (!isPublicKey && keyType != PrivateKey.class) {
|
|
||||||
throw new RuntimeException("I can only read public and private keys!");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] key = Files.readAllBytes(fileLocation);
|
|
||||||
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
||||||
EncodedKeySpec keySpec = isPublicKey ? new X509EncodedKeySpec(key) : new PKCS8EncodedKeySpec(key);
|
|
||||||
return (T) (isPublicKey ?
|
|
||||||
keyFactory.generatePublic(keySpec) :
|
|
||||||
keyFactory.generatePrivate(keySpec)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.util;
|
||||||
|
|
||||||
|
public class InvalidHeaderException extends Exception {
|
||||||
|
public InvalidHeaderException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidHeaderException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidHeaderException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,9 +26,7 @@
|
||||||
|
|
||||||
package org.geysermc.floodgate.util;
|
package org.geysermc.floodgate.util;
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -50,7 +48,6 @@ public final class LinkedPlayer {
|
||||||
* If the LinkedPlayer is send from a different platform.
|
* If the LinkedPlayer is send from a different platform.
|
||||||
* For example the LinkedPlayer is from Bungee but the data has been sent to the Bukkit server.
|
* For example the LinkedPlayer is from Bungee but the data has been sent to the Bukkit server.
|
||||||
*/
|
*/
|
||||||
@Setter(AccessLevel.PRIVATE)
|
|
||||||
private boolean fromDifferentPlatform = false;
|
private boolean fromDifferentPlatform = false;
|
||||||
|
|
||||||
public LinkedPlayer(String javaUsername, UUID javaUniqueId, UUID bedrockId) {
|
public LinkedPlayer(String javaUsername, UUID javaUniqueId, UUID bedrockId) {
|
||||||
|
@ -60,12 +57,15 @@ public final class LinkedPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
static LinkedPlayer fromString(String data) {
|
static LinkedPlayer fromString(String data) {
|
||||||
if (data.length() == 4) return null;
|
if (data.length() != 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String[] split = data.split(";");
|
String[] split = data.split(";");
|
||||||
LinkedPlayer player = new LinkedPlayer(
|
LinkedPlayer player = new LinkedPlayer(
|
||||||
split[0], UUID.fromString(split[1]), UUID.fromString(split[2])
|
split[0], UUID.fromString(split[1]), UUID.fromString(split[2])
|
||||||
);
|
);
|
||||||
player.setFromDifferentPlatform(true);
|
player.fromDifferentPlatform = true;
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,12 +55,16 @@ import org.geysermc.connector.network.translators.world.block.entity.BlockEntity
|
||||||
import org.geysermc.connector.utils.DimensionUtils;
|
import org.geysermc.connector.utils.DimensionUtils;
|
||||||
import org.geysermc.connector.utils.LanguageUtils;
|
import org.geysermc.connector.utils.LanguageUtils;
|
||||||
import org.geysermc.connector.utils.LocaleUtils;
|
import org.geysermc.connector.utils.LocaleUtils;
|
||||||
|
import org.geysermc.floodgate.crypto.AesCipher;
|
||||||
|
import org.geysermc.floodgate.crypto.AesKeyProducer;
|
||||||
|
import org.geysermc.floodgate.crypto.FloodgateCipher;
|
||||||
|
|
||||||
import javax.naming.directory.Attribute;
|
import javax.naming.directory.Attribute;
|
||||||
import javax.naming.directory.InitialDirContext;
|
import javax.naming.directory.InitialDirContext;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.security.Key;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -87,6 +91,8 @@ public class GeyserConnector {
|
||||||
@Setter
|
@Setter
|
||||||
private AuthType authType;
|
private AuthType authType;
|
||||||
|
|
||||||
|
private FloodgateCipher cipher;
|
||||||
|
|
||||||
private boolean shuttingDown = false;
|
private boolean shuttingDown = false;
|
||||||
|
|
||||||
private final ScheduledExecutorService generalThreadPool;
|
private final ScheduledExecutorService generalThreadPool;
|
||||||
|
@ -171,6 +177,17 @@ public class GeyserConnector {
|
||||||
remoteServer = new RemoteServer(config.getRemote().getAddress(), remotePort);
|
remoteServer = new RemoteServer(config.getRemote().getAddress(), remotePort);
|
||||||
authType = AuthType.getByName(config.getRemote().getAuthType());
|
authType = AuthType.getByName(config.getRemote().getAuthType());
|
||||||
|
|
||||||
|
if (authType == AuthType.FLOODGATE) {
|
||||||
|
try {
|
||||||
|
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyFile());
|
||||||
|
cipher = new AesCipher();
|
||||||
|
cipher.init(key);
|
||||||
|
logger.info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
logger.severe(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.isAboveBedrockNetherBuilding())
|
if (config.isAboveBedrockNetherBuilding())
|
||||||
DimensionUtils.changeBedrockNetherId(); // Apply End dimension ID workaround to Nether
|
DimensionUtils.changeBedrockNetherId(); // Apply End dimension ID workaround to Nether
|
||||||
|
|
||||||
|
|
|
@ -73,15 +73,19 @@ import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
|
||||||
import org.geysermc.connector.network.translators.item.ItemRegistry;
|
import org.geysermc.connector.network.translators.item.ItemRegistry;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
||||||
import org.geysermc.connector.utils.*;
|
import org.geysermc.connector.utils.*;
|
||||||
|
import org.geysermc.floodgate.crypto.FloodgateCipher;
|
||||||
import org.geysermc.floodgate.util.BedrockData;
|
import org.geysermc.floodgate.util.BedrockData;
|
||||||
import org.geysermc.floodgate.util.EncryptionUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -336,24 +340,6 @@ public class GeyserSession implements CommandSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE;
|
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE;
|
||||||
final PublicKey publicKey;
|
|
||||||
|
|
||||||
if (floodgate) {
|
|
||||||
PublicKey key = null;
|
|
||||||
try {
|
|
||||||
key = EncryptionUtil.getKeyFromFile(
|
|
||||||
connector.getConfig().getFloodgateKeyFile(),
|
|
||||||
PublicKey.class
|
|
||||||
);
|
|
||||||
} catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
|
|
||||||
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e);
|
|
||||||
}
|
|
||||||
publicKey = key;
|
|
||||||
} else publicKey = null;
|
|
||||||
|
|
||||||
if (publicKey != null) {
|
|
||||||
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
|
|
||||||
}
|
|
||||||
|
|
||||||
downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory());
|
downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory());
|
||||||
downstream.getSession().addListener(new SessionAdapter() {
|
downstream.getSession().addListener(new SessionAdapter() {
|
||||||
|
@ -361,9 +347,11 @@ public class GeyserSession implements CommandSender {
|
||||||
public void packetSending(PacketSendingEvent event) {
|
public void packetSending(PacketSendingEvent event) {
|
||||||
//todo move this somewhere else
|
//todo move this somewhere else
|
||||||
if (event.getPacket() instanceof HandshakePacket && floodgate) {
|
if (event.getPacket() instanceof HandshakePacket && floodgate) {
|
||||||
String encrypted = "";
|
byte[] encryptedData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData(
|
FloodgateCipher cipher = connector.getCipher();
|
||||||
|
encryptedData = cipher.encryptFromString(new BedrockData(
|
||||||
clientData.getGameVersion(),
|
clientData.getGameVersion(),
|
||||||
authData.getName(),
|
authData.getName(),
|
||||||
authData.getXboxUUID(),
|
authData.getXboxUUID(),
|
||||||
|
@ -373,15 +361,22 @@ public class GeyserSession implements CommandSender {
|
||||||
clientData.getCurrentInputMode().ordinal(),
|
clientData.getCurrentInputMode().ordinal(),
|
||||||
upstream.getSession().getAddress().getAddress().getHostAddress(),
|
upstream.getSession().getAddress().getAddress().getHostAddress(),
|
||||||
clientData.getImage("Skin")
|
clientData.getImage("Skin")
|
||||||
));
|
).toString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
|
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
|
||||||
|
disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.floodgate.encryption_fail", getClientData().getLanguageCode()));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String encrypted = new String(
|
||||||
|
Base64.getEncoder().encode(encryptedData),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
|
||||||
HandshakePacket handshakePacket = event.getPacket();
|
HandshakePacket handshakePacket = event.getPacket();
|
||||||
event.setPacket(new HandshakePacket(
|
event.setPacket(new HandshakePacket(
|
||||||
handshakePacket.getProtocolVersion(),
|
handshakePacket.getProtocolVersion(),
|
||||||
handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted,
|
handshakePacket.getHostname() + '\0' + encrypted,
|
||||||
handshakePacket.getPort(),
|
handshakePacket.getPort(),
|
||||||
handshakePacket.getIntent()
|
handshakePacket.getIntent()
|
||||||
));
|
));
|
||||||
|
|
Loading…
Reference in a new issue