Merge remote-tracking branch 'origin/feature/floodgate-data-version' into feature/1.18

This commit is contained in:
Camotoy 2021-11-30 11:09:16 -05:00
commit 7df013daf9
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
15 changed files with 46 additions and 296 deletions

View file

@ -70,8 +70,8 @@ public final class AesCipher implements FloodgateCipher {
cipherText = topping.encode(cipherText);
}
return ByteBuffer.allocate(iv.length + cipherText.length + HEADER_LENGTH + 1)
.put(IDENTIFIER) // header
return ByteBuffer.allocate(HEADER.length + iv.length + cipherText.length + 1)
.put(HEADER)
.put(iv)
.put((byte) 0x21)
.put(cipherText)
@ -83,15 +83,15 @@ public final class AesCipher implements FloodgateCipher {
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
int bufferLength = cipherTextWithIv.length - HEADER_LENGTH;
ByteBuffer buffer = ByteBuffer.wrap(cipherTextWithIv, HEADER_LENGTH, bufferLength);
int bufferLength = cipherTextWithIv.length - HEADER.length;
ByteBuffer buffer = ByteBuffer.wrap(cipherTextWithIv, HEADER.length, bufferLength);
int ivLength = IV_LENGTH;
if (topping != null) {
int mark = buffer.position();
// we need the first index, the second is for the optional RawSkin
// we need the first index, the second is for the actual data
boolean found = false;
while (buffer.hasRemaining() && !found) {
if (buffer.get() == 0x21) {

View file

@ -39,7 +39,7 @@ public final class AesKeyProducer implements KeyProducer {
public SecretKey produce() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(KEY_SIZE, getSecureRandom());
keyGenerator.init(KEY_SIZE, secureRandom());
return keyGenerator.generateKey();
} catch (Exception exception) {
throw new RuntimeException(exception);
@ -55,7 +55,7 @@ public final class AesKeyProducer implements KeyProducer {
}
}
private SecureRandom getSecureRandom() throws NoSuchAlgorithmException {
private SecureRandom secureRandom() throws NoSuchAlgorithmException {
// use Windows-PRNG for windows (default impl is SHA1PRNG)
if (System.getProperty("os.name").startsWith("Windows")) {
return SecureRandom.getInstance("Windows-PRNG");

View file

@ -26,33 +26,32 @@
package org.geysermc.floodgate.crypto;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.geysermc.floodgate.util.InvalidFormatException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Responsible for both encrypting and decrypting data
*/
public interface FloodgateCipher {
// use invalid username characters at the beginning and the end of the identifier,
// to make sure that it doesn't get messed up with usernames
byte[] IDENTIFIER = "^Floodgate^".getBytes(StandardCharsets.UTF_8);
int HEADER_LENGTH = IDENTIFIER.length;
int VERSION = 0;
byte[] IDENTIFIER = "^Floodgate^".getBytes(UTF_8);
byte[] HEADER = (new String(IDENTIFIER, UTF_8) + (char) (VERSION + 0x3E)).getBytes(UTF_8);
static boolean hasHeader(String data) {
if (data.length() < IDENTIFIER.length) {
return false;
static int version(String data) {
if (data.length() <= HEADER.length) {
return -1;
}
for (int i = 0; i < IDENTIFIER.length; i++) {
if (IDENTIFIER[i] != data.charAt(i)) {
return false;
return -1;
}
}
return true;
return data.charAt(IDENTIFIER.length) - 0x3E;
}
/**
@ -79,7 +78,7 @@ public interface FloodgateCipher {
* @throws Exception when the encryption failed
*/
default byte[] encryptFromString(String data) throws Exception {
return encrypt(data.getBytes(StandardCharsets.UTF_8));
return encrypt(data.getBytes(UTF_8));
}
/**
@ -104,7 +103,7 @@ public interface FloodgateCipher {
if (decrypted == null) {
return null;
}
return new String(decrypted, StandardCharsets.UTF_8);
return new String(decrypted, UTF_8);
}
/**
@ -116,7 +115,7 @@ public interface FloodgateCipher {
* @throws Exception when the decrypting failed
*/
default byte[] decryptFromString(String data) throws Exception {
return decrypt(data.getBytes(StandardCharsets.UTF_8));
return decrypt(data.getBytes(UTF_8));
}
/**
@ -127,37 +126,21 @@ public interface FloodgateCipher {
* @throws InvalidFormatException when the header is invalid
*/
default void checkHeader(byte[] data) throws InvalidFormatException {
final int identifierLength = IDENTIFIER.length;
if (data.length <= HEADER_LENGTH) {
throw new InvalidFormatException("Data length is smaller then header." +
"Needed " + HEADER_LENGTH + ", got " + data.length,
true
if (data.length <= HEADER.length) {
throw new InvalidFormatException(
"Data length is smaller then header." +
"Needed " + HEADER.length + ", got " + data.length
);
}
for (int i = 0; i < identifierLength; i++) {
for (int i = 0; i < IDENTIFIER.length; i++) {
if (IDENTIFIER[i] != data[i]) {
StringBuilder receivedIdentifier = new StringBuilder();
for (byte b : IDENTIFIER) {
receivedIdentifier.append(b);
}
String identifier = new String(IDENTIFIER, UTF_8);
String received = new String(data, 0, IDENTIFIER.length, UTF_8);
throw new InvalidFormatException(
String.format("Expected identifier %s, got %s",
new String(IDENTIFIER, StandardCharsets.UTF_8),
receivedIdentifier.toString()
),
true
"Expected identifier " + identifier + ", got " + received
);
}
}
}
@Data
@AllArgsConstructor
class HeaderResult {
private int version;
private int startIndex;
}
}

View file

@ -1,91 +0,0 @@
/*
* Copyright (c) 2019-2021 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.time;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.ByteBuffer;
/*
* Thanks:
* https://datatracker.ietf.org/doc/html/rfc1769
* https://github.com/jonsagara/SimpleNtpClient
* https://stackoverflow.com/a/29138806
*/
public final class SntpClientUtils {
private static final int NTP_PORT = 123;
private static final int NTP_PACKET_SIZE = 48;
private static final int NTP_MODE = 3; // client
private static final int NTP_VERSION = 3;
private static final int RECEIVE_TIME_POSITION = 32;
private static final long NTP_TIME_OFFSET = ((365L * 70L) + 17L) * 24L * 60L * 60L;
public static long requestTimeOffset(String host, int timeout) {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(timeout);
InetAddress address = InetAddress.getByName(host);
ByteBuffer buff = ByteBuffer.allocate(NTP_PACKET_SIZE);
DatagramPacket request = new DatagramPacket(
buff.array(), NTP_PACKET_SIZE, address, NTP_PORT
);
// mode is in the least signification 3 bits
// version is in bits 3-5
buff.put((byte) (NTP_MODE | (NTP_VERSION << 3)));
long originateTime = System.currentTimeMillis();
socket.send(request);
DatagramPacket response = new DatagramPacket(buff.array(), NTP_PACKET_SIZE);
socket.receive(response);
long responseTime = System.currentTimeMillis();
// everything before isn't important for us
buff.position(RECEIVE_TIME_POSITION);
long receiveTime = readTimestamp(buff);
long transmitTime = readTimestamp(buff);
return ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2;
} catch (Exception ignored) {
}
return Long.MIN_VALUE;
}
private static long readTimestamp(ByteBuffer buffer) {
//todo look into the ntp 2036 problem
long seconds = buffer.getInt() & 0xffffffffL;
long fraction = buffer.getInt() & 0xffffffffL;
return ((seconds - NTP_TIME_OFFSET) * 1000) + ((fraction * 1000) / 0x100000000L);
}
}

View file

@ -1,67 +0,0 @@
/*
* Copyright (c) 2019-2021 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.time;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public final class TimeSyncer {
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
private long timeOffset = Long.MIN_VALUE; // value when it failed to get the offset
public TimeSyncer(String timeServer) {
executorService.scheduleWithFixedDelay(() -> {
// 5 tries to get the time offset, since UDP doesn't guaranty a response
for (int i = 0; i < 5; i++) {
long offset = SntpClientUtils.requestTimeOffset(timeServer, 3000);
if (offset != Long.MIN_VALUE) {
timeOffset = offset;
return;
}
}
}, 0, 30, TimeUnit.MINUTES);
}
public void shutdown() {
executorService.shutdown();
}
public long getTimeOffset() {
return timeOffset;
}
public long getRealMillis() {
if (hasUsefulOffset()) {
return System.currentTimeMillis() + getTimeOffset();
}
return System.currentTimeMillis();
}
public boolean hasUsefulOffset() {
return timeOffset != Long.MIN_VALUE;
}
}

View file

@ -1,35 +0,0 @@
/*
* Copyright (c) 2019-2021 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 final class Base64Utils {
public static int getEncodedLength(int length) {
if (length <= 0) {
return -1;
}
return 4 * ((length + 2) / 3);
}
}

View file

@ -28,7 +28,6 @@ package org.geysermc.floodgate.util;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.geysermc.floodgate.time.TimeSyncer;
/**
* This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This
@ -38,7 +37,7 @@ import org.geysermc.floodgate.time.TimeSyncer;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class BedrockData implements Cloneable {
public static final int EXPECTED_LENGTH = 13;
public static final int EXPECTED_LENGTH = 12;
private final String version;
private final String username;
@ -54,25 +53,23 @@ public final class BedrockData implements Cloneable {
private final int subscribeId;
private final String verifyCode;
private final long timestamp;
private final int dataLength;
public static BedrockData of(
String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip,
LinkedPlayer linkedPlayer, boolean fromProxy, int subscribeId,
String verifyCode, TimeSyncer timeSyncer) {
String verifyCode) {
return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode,
uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode,
timeSyncer.getRealMillis(), EXPECTED_LENGTH);
uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode, EXPECTED_LENGTH);
}
public static BedrockData of(
String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip,
int subscribeId, String verifyCode, TimeSyncer timeSyncer) {
int subscribeId, String verifyCode) {
return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null,
false, subscribeId, verifyCode, timeSyncer);
false, subscribeId, verifyCode);
}
public static BedrockData fromString(String data) {
@ -86,12 +83,12 @@ public final class BedrockData implements Cloneable {
return new BedrockData(
split[0], split[1], split[2], Integer.parseInt(split[3]), split[4],
Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], linkedPlayer,
"1".equals(split[9]), Integer.parseInt(split[10]), split[11], Long.parseLong(split[12]), split.length
"1".equals(split[9]), Integer.parseInt(split[10]), split[11], split.length
);
}
private static BedrockData emptyData(int dataLength) {
return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, -1, null, -1,
return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, -1, null,
dataLength);
}
@ -105,7 +102,7 @@ public final class BedrockData implements Cloneable {
return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' +
languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' +
(linkedPlayer != null ? linkedPlayer.toString() : "null") + '\0' +
(fromProxy ? 1 : 0) + '\0' + subscribeId + '\0' + verifyCode + '\0' + timestamp;
(fromProxy ? 1 : 0) + '\0' + subscribeId + '\0' + verifyCode;
}
@Override

View file

@ -59,7 +59,7 @@ public enum DeviceOs {
* @param id the DeviceOs identifier
* @return The DeviceOs or {@link #UNKNOWN} if the DeviceOs wasn't found
*/
public static DeviceOs getById(int id) {
public static DeviceOs fromId(int id) {
return id < VALUES.length ? VALUES[id] : VALUES[0];
}

View file

@ -41,7 +41,7 @@ public enum InputMode {
* @param id the InputMode identifier
* @return The InputMode or {@link #UNKNOWN} if the DeviceOs wasn't found
*/
public static InputMode getById(int id) {
public static InputMode fromId(int id) {
return VALUES.length > id ? VALUES[id] : VALUES[0];
}
}

View file

@ -26,26 +26,8 @@
package org.geysermc.floodgate.util;
import lombok.Getter;
@Getter
public class InvalidFormatException extends Exception {
private boolean header = false;
public InvalidFormatException() {
super();
}
public InvalidFormatException(String message) {
super(message);
}
public InvalidFormatException(String message, boolean header) {
super(message);
this.header = header;
}
public InvalidFormatException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -38,7 +38,7 @@ public enum UiProfile {
* @param id the UiProfile identifier
* @return The UiProfile or {@link #CLASSIC} if the UiProfile wasn't found
*/
public static UiProfile getById(int id) {
public static UiProfile fromId(int id) {
return VALUES.length > id ? VALUES[id] : VALUES[0];
}
}

View file

@ -81,11 +81,11 @@ public enum WebsocketEventType {
this.id = id;
}
public static WebsocketEventType getById(int id) {
public static WebsocketEventType fromId(int id) {
return VALUES.length > id ? VALUES[id] : null;
}
public int getId() {
public int id() {
return id;
}
}

View file

@ -48,7 +48,6 @@ import org.geysermc.floodgate.crypto.AesKeyProducer;
import org.geysermc.floodgate.crypto.Base64Topping;
import org.geysermc.floodgate.crypto.FloodgateCipher;
import org.geysermc.floodgate.news.NewsItemAction;
import org.geysermc.floodgate.time.TimeSyncer;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.command.CommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
@ -110,7 +109,6 @@ public class GeyserImpl implements GeyserApi {
@Setter
private static boolean shouldStartListener = true;
private final TimeSyncer timeSyncer;
private FloodgateCipher cipher;
private FloodgateSkinUploader skinUploader;
private final NewsHandler newsHandler;
@ -201,9 +199,7 @@ public class GeyserImpl implements GeyserApi {
// Ensure that PacketLib does not create an event loop for handling packets; we'll do that ourselves
TcpSession.USE_EVENT_LOOP_FOR_PACKETS = false;
TimeSyncer timeSyncer = null;
if (config.getRemote().getAuthType() == AuthType.FLOODGATE) {
timeSyncer = new TimeSyncer(Constants.NTP_SERVER);
try {
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
cipher = new AesCipher(new Base64Topping());
@ -214,7 +210,6 @@ public class GeyserImpl implements GeyserApi {
logger.severe(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.bad_key"), exception);
}
}
this.timeSyncer = timeSyncer;
String branch = "unknown";
int buildNumber = -1;
@ -441,9 +436,6 @@ public class GeyserImpl implements GeyserApi {
scheduledThread.shutdown();
bedrockServer.close();
if (timeSyncer != null) {
timeSyncer.shutdown();
}
if (skinUploader != null) {
skinUploader.close();
}
@ -491,10 +483,6 @@ public class GeyserImpl implements GeyserApi {
return bootstrap.getWorldManager();
}
public TimeSyncer getTimeSyncer() {
return timeSyncer;
}
public static GeyserImpl getInstance() {
return instance;
}

View file

@ -787,6 +787,8 @@ public class GeyserSession implements GeyserConnection, CommandSender {
FloodgateSkinUploader skinUploader = geyser.getSkinUploader();
FloodgateCipher cipher = geyser.getCipher();
System.out.println(new String(FloodgateCipher.HEADER, StandardCharsets.UTF_8));
encryptedData = cipher.encryptFromString(BedrockData.of(
clientData.getGameVersion(),
authData.name(),
@ -797,17 +799,8 @@ public class GeyserSession implements GeyserConnection, CommandSender {
clientData.getCurrentInputMode().ordinal(),
upstream.getAddress().getAddress().getHostAddress(),
skinUploader.getId(),
skinUploader.getVerifyCode(),
geyser.getTimeSyncer()
skinUploader.getVerifyCode()
).toString());
if (!geyser.getTimeSyncer().hasUsefulOffset()) {
geyser.getLogger().warning(
"We couldn't make sure that your system clock is accurate. " +
"This can cause issues with logging in."
);
}
} catch (Exception e) {
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.floodgate.encryption_fail", getClientData().getLanguageCode()));

View file

@ -87,7 +87,7 @@ public final class FloodgateSkinUploader {
}
int typeId = node.get("event_id").asInt();
WebsocketEventType type = WebsocketEventType.getById(typeId);
WebsocketEventType type = WebsocketEventType.fromId(typeId);
if (type == null) {
logger.warning(String.format(
"Got (unknown) type %s. Ensure that Geyser is on the latest version and report this issue!",