Skip to content

Commit

Permalink
Implement Minecraft Education v1.12.60 support
Browse files Browse the repository at this point in the history
Changes:
  * Support both MCEE and MCPE versions.
  * Config entry to enable/disable education support
  * Enable 2 way encryption by default now whilst still performing mitm
  • Loading branch information
bundabrg committed May 12, 2020
1 parent e2d98c4 commit eb76a94
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 17 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,6 @@ config.yml
logs/
logs/*
packet-logs/
packet-logs/*
packet-logs/*
data/*
sessions/*
10 changes: 9 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,18 @@
<version>1.3.9</version>
<scope>provided</scope>
</dependency>
<!-- Current Bedrock Version -->
<dependency>
<groupId>com.nukkitx.protocol</groupId>
<artifactId>bedrock-v390</artifactId>
<version>2.5.5</version>
<version>2.5.7-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- Current Education Version -->
<dependency>
<groupId>com.nukkitx.protocol</groupId>
<artifactId>bedrock-v363</artifactId>
<version>2.5.7-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/nukkitx/proxypass/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class Configuration {
@JsonProperty("log-packets")
private boolean loggingPackets = false;

@JsonProperty("education")
private boolean education = false;

@JsonProperty("ignored-packets")
private Set<String> ignoredPackets = Collections.emptySet();

Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/nukkitx/proxypass/ProxyPass.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.nukkitx.protocol.bedrock.BedrockClient;
import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
import com.nukkitx.protocol.bedrock.BedrockServer;
import com.nukkitx.protocol.bedrock.v363.Bedrock_v363;
import com.nukkitx.protocol.bedrock.v390.Bedrock_v390;
import com.nukkitx.proxypass.network.ProxyBedrockEventHandler;
import lombok.AccessLevel;
Expand All @@ -37,8 +38,6 @@ public class ProxyPass {
public static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
public static final YAMLMapper YAML_MAPPER = (YAMLMapper) new YAMLMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
public static final String MINECRAFT_VERSION;
public static final BedrockPacketCodec CODEC = Bedrock_v390.V390_CODEC;
public static final int PROTOCOL_VERSION = CODEC.getProtocolVersion();
private static final DefaultPrettyPrinter PRETTY_PRINTER = new DefaultPrettyPrinter();

static {
Expand All @@ -56,6 +55,8 @@ public class ProxyPass {
MINECRAFT_VERSION = minecraftVersion;
}

public BedrockPacketCodec CODEC;
public int PROTOCOL_VERSION;
private final AtomicBoolean running = new AtomicBoolean(true);
private BedrockServer bedrockServer;
private final Set<BedrockClient> clients = Collections.newSetFromMap(new ConcurrentHashMap<>());
Expand Down Expand Up @@ -86,6 +87,14 @@ public void boot() throws IOException {

configuration = Configuration.load(configPath);

if (configuration.isEducation()) {
CODEC = Bedrock_v363.V363_CODEC;
} else {
CODEC = Bedrock_v390.V390_CODEC;
}

PROTOCOL_VERSION = CODEC.getProtocolVersion();

proxyAddress = configuration.getProxy().getAddress();
targetAddress = configuration.getDestination().getAddress();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,28 @@
import java.net.InetSocketAddress;

@Log4j2
@RequiredArgsConstructor
@ParametersAreNonnullByDefault
public class ProxyBedrockEventHandler implements BedrockServerEventHandler {
private static final BedrockPong ADVERTISEMENT = new BedrockPong();

private final ProxyPass proxy;
private final BedrockPong ADVERTISEMENT = new BedrockPong();

static {
ADVERTISEMENT.setEdition("MCPE");
public ProxyBedrockEventHandler(ProxyPass proxy) {
this.proxy = proxy;

if (proxy.getConfiguration().isEducation()) {
ADVERTISEMENT.setEdition("MCEE");
} else {
ADVERTISEMENT.setEdition("MCPE");
}
ADVERTISEMENT.setGameType("Survival");
ADVERTISEMENT.setVersion(ProxyPass.MINECRAFT_VERSION);
ADVERTISEMENT.setProtocolVersion(ProxyPass.PROTOCOL_VERSION);
ADVERTISEMENT.setProtocolVersion(proxy.PROTOCOL_VERSION);
ADVERTISEMENT.setMotd("ProxyPass");
ADVERTISEMENT.setPlayerCount(0);
ADVERTISEMENT.setMaximumPlayerCount(20);
ADVERTISEMENT.setSubMotd("https://github.com/NukkitX/ProxyPass");

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.nukkitx.protocol.bedrock.util.EncryptionUtils;
import com.nukkitx.proxypass.ProxyPass;
import com.nukkitx.proxypass.network.bedrock.util.BlockPaletteUtils;
import com.nukkitx.proxypass.network.bedrock.util.ForgeryUtils;
import com.nukkitx.proxypass.network.bedrock.util.RecipeUtils;
import lombok.RequiredArgsConstructor;
import lombok.Value;
Expand Down Expand Up @@ -46,6 +47,17 @@ public boolean handle(ServerToClientHandshakePacket packet) {
SecretKey key = EncryptionUtils.getSecretKey(this.player.getProxyKeyPair().getPrivate(), serverKey,
Base64.getDecoder().decode(saltJwt.getJWTClaimsSet().getStringClaim("salt")));
session.enableEncryption(key);

ServerToClientHandshakePacket p = new ServerToClientHandshakePacket();
p.setJwt(ForgeryUtils.forgeHandshake(
player.getProxyKeyPair(),
saltJwt.getJWTClaimsSet().getStringClaim("signedToken"),
Base64.getDecoder().decode(saltJwt.getJWTClaimsSet().getStringClaim("salt"))).serialize()
);
player.getUpstream().sendPacketImmediately(p);
player.getUpstream().enableEncryption(EncryptionUtils.getSecretKey(player.getProxyKeyPair().getPrivate(),
((UpstreamPacketHandler)player.getUpstream().getPacketHandler()).getRemotePublicKey(),Base64.getDecoder().decode(saltJwt.getJWTClaimsSet().getStringClaim("salt"))));

} catch (ParseException | NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ public void handle(BedrockSession session, ByteBuf compressed, Collection<Bedroc
}

if (packetTesting) {
ByteBuf buffer = ProxyPass.CODEC.tryEncode(packet);
ByteBuf buffer = proxy.CODEC.tryEncode(packet);
try {
BedrockPacket packet2 = ProxyPass.CODEC.tryDecode(buffer);
BedrockPacket packet2 = proxy.CODEC.tryDecode(buffer);
if (!Objects.equals(packet, packet2)) {
// Something went wrong in serialization.
log.warn("Packets instances not equal:\n Original : {}\nRe-encoded : {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
import com.nimbusds.jwt.SignedJWT;
import com.nukkitx.protocol.bedrock.BedrockServerSession;
import com.nukkitx.protocol.bedrock.handler.BedrockPacketHandler;
import com.nukkitx.protocol.bedrock.packet.ClientToServerHandshakePacket;
import com.nukkitx.protocol.bedrock.packet.LoginPacket;
import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket;
import com.nukkitx.protocol.bedrock.util.EncryptionUtils;
import com.nukkitx.proxypass.ProxyPass;
import com.nukkitx.proxypass.network.bedrock.util.ForgeryUtils;
import io.netty.util.AsciiString;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import net.minidev.json.JSONObject;
Expand All @@ -36,6 +38,8 @@ public class UpstreamPacketHandler implements BedrockPacketHandler {
private ArrayNode chainData;
private AuthData authData;
private ProxyPlayerSession player;
@Getter
private ECPublicKey remotePublicKey;

private static boolean validateChainData(JsonNode data) throws Exception {
ECPublicKey lastKey = null;
Expand Down Expand Up @@ -63,19 +67,24 @@ private static boolean verifyJwt(JWSObject jwt, ECPublicKey key) throws JOSEExce
return jwt.verify(new DefaultJWSVerifierFactory().createJWSVerifier(jwt.getHeader(), key));
}

public boolean handle(ClientToServerHandshakePacket packet) {
// This is handled ourselves and we don't want a duplicate packet
return true;
}

@Override
public boolean handle(LoginPacket packet) {
int protocolVersion = packet.getProtocolVersion();

if (protocolVersion != ProxyPass.PROTOCOL_VERSION) {
if (protocolVersion != proxy.PROTOCOL_VERSION) {
PlayStatusPacket status = new PlayStatusPacket();
if (protocolVersion > ProxyPass.PROTOCOL_VERSION) {
if (protocolVersion > proxy.PROTOCOL_VERSION) {
status.setStatus(PlayStatusPacket.Status.FAILED_SERVER);
} else {
status.setStatus(PlayStatusPacket.Status.FAILED_CLIENT);
}
}
session.setPacketCodec(ProxyPass.CODEC);
session.setPacketCodec(proxy.CODEC);

JsonNode certData;
try {
Expand Down Expand Up @@ -111,6 +120,7 @@ public boolean handle(LoginPacket packet) {
throw new RuntimeException("Identity Public Key was not found!");
}
ECPublicKey identityPublicKey = EncryptionUtils.generateKey(payload.get("identityPublicKey").textValue());
this.remotePublicKey = identityPublicKey;

JWSObject clientJwt = JWSObject.parse(packet.getSkinData().toString());
verifyJwt(clientJwt, identityPublicKey);
Expand All @@ -131,7 +141,7 @@ private void initializeProxySession() {
log.error("Unable to connect to downstream server " + proxy.getTargetAddress(), throwable);
return;
}
downstream.setPacketCodec(ProxyPass.CODEC);
downstream.setPacketCodec(proxy.CODEC);
ProxyPlayerSession proxySession = new ProxyPlayerSession(this.session, downstream, this.proxy, this.authData);
this.player = proxySession;

Expand All @@ -150,7 +160,7 @@ private void initializeProxySession() {
LoginPacket login = new LoginPacket();
login.setChainData(chainData);
login.setSkinData(AsciiString.of(skinData.serialize()));
login.setProtocolVersion(ProxyPass.PROTOCOL_VERSION);
login.setProtocolVersion(proxy.PROTOCOL_VERSION);

downstream.sendPacketImmediately(login);
this.session.setBatchedHandler(proxySession.getUpstreamBatchHandler());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,32 @@ public static JWSObject forgeSkinData(KeyPair pair, JSONObject skinData) {

return jws;
}

public static SignedJWT forgeHandshake(KeyPair pair, String signedToken, byte[] token) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
URI x5u = URI.create(publicKeyBase64);

JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES384).x509CertURL(x5u).build();

long timestamp = System.currentTimeMillis();
Date nbf = new Date(timestamp - TimeUnit.SECONDS.toMillis(1));
Date exp = new Date(timestamp + TimeUnit.DAYS.toMillis(1));

JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.claim("salt", Base64.getEncoder().encodeToString(token));

if (signedToken != null) {
claimsBuilder.claim("signedToken", signedToken);
}

SignedJWT jwt = new SignedJWT(header, claimsBuilder.build());

try {
EncryptionUtils.signJwt(jwt, (ECPrivateKey) pair.getPrivate());
} catch (JOSEException e) {
throw new RuntimeException(e);
}

return jwt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public static void writeRecipes(CraftingDataPacket packet, ProxyPass proxy) {
entries.add(entry);
}

Recipes recipes = new Recipes(ProxyPass.CODEC.getProtocolVersion(), entries, packet.getPotionMixData(), packet.getContainerMixData());
Recipes recipes = new Recipes(proxy.CODEC.getProtocolVersion(), entries, packet.getPotionMixData(), packet.getContainerMixData());

proxy.saveJson("recipes.json", recipes);
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ destination:
packet-testing: false
## Log packets for each session
log-packets: true
## Minecraft Education Support
education: false

## Packets to ignore to make your log more refined. These default packet are generally spammed
ignored-packets:
Expand Down

0 comments on commit eb76a94

Please sign in to comment.