package net.glowstone.net.handler.login;
import com.flowpowered.networking.MessageHandler;
import net.glowstone.EventFactory;
import net.glowstone.GlowServer;
import net.glowstone.entity.meta.profile.PlayerProfile;
import net.glowstone.entity.meta.profile.PlayerProperty;
import net.glowstone.net.GlowSession;
import net.glowstone.net.message.login.EncryptionKeyResponseMessage;
import net.glowstone.util.UuidUtils;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
public final class EncryptionKeyResponseHandler implements MessageHandler<GlowSession, EncryptionKeyResponseMessage> {
@Override
public void handle(GlowSession session, EncryptionKeyResponseMessage message) {
final PrivateKey privateKey = session.getServer().getKeyPair().getPrivate();
// create rsaCipher
Cipher rsaCipher;
try {
rsaCipher = Cipher.getInstance("RSA");
} catch (GeneralSecurityException ex) {
GlowServer.logger.log(Level.SEVERE, "Could not initialize RSA cipher", ex);
session.disconnect("Unable to initialize RSA cipher.");
return;
}
// decrypt shared secret
SecretKey sharedSecret;
try {
rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
sharedSecret = new SecretKeySpec(rsaCipher.doFinal(message.getSharedSecret()), "AES");
} catch (Exception ex) {
GlowServer.logger.log(Level.WARNING, "Could not decrypt shared secret", ex);
session.disconnect("Unable to decrypt shared secret.");
return;
}
// decrypt verify token
byte[] verifyToken;
try {
rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
verifyToken = rsaCipher.doFinal(message.getVerifyToken());
} catch (Exception ex) {
GlowServer.logger.log(Level.WARNING, "Could not decrypt verify token", ex);
session.disconnect("Unable to decrypt verify token.");
return;
}
// check verify token
if (!Arrays.equals(verifyToken, session.getVerifyToken())) {
session.disconnect("Invalid verify token.");
return;
}
// initialize stream encryption
session.enableEncryption(sharedSecret);
// create hash for auth
String hash;
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(session.getSessionId().getBytes());
digest.update(sharedSecret.getEncoded());
digest.update(session.getServer().getKeyPair().getPublic().getEncoded());
// BigInteger takes care of sign and leading zeroes
hash = new BigInteger(digest.digest()).toString(16);
} catch (NoSuchAlgorithmException ex) {
GlowServer.logger.log(Level.SEVERE, "Unable to generate SHA-1 digest", ex);
session.disconnect("Failed to hash login data.");
return;
}
// start auth thread
Thread clientAuthThread = new Thread(new ClientAuthRunnable(session, session.getVerifyUsername(), hash));
clientAuthThread.setName("ClientAuthThread{" + session.getVerifyUsername() + "}");
clientAuthThread.start();
}
private static class ClientAuthRunnable implements Runnable {
private static final String BASE_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined";
private final GlowSession session;
private final String username;
private final String postURL;
private ClientAuthRunnable(GlowSession session, String username, String hash) {
this.session = session;
this.username = username;
this.postURL = BASE_URL + "?username=" + username + "&serverId=" + hash;
}
@Override
public void run() {
try {
// authenticate
URLConnection connection = new URL(postURL).openConnection();
JSONObject json;
try (InputStream is = connection.getInputStream()) {
try {
json = (JSONObject) new JSONParser().parse(new InputStreamReader(is));
} catch (ParseException e) {
GlowServer.logger.warning("Username \"" + username + "\" failed to authenticate!");
session.disconnect("Failed to verify username!");
return;
}
}
final String name = (String) json.get("name");
final String id = (String) json.get("id");
// parse UUID
final UUID uuid;
try {
uuid = UuidUtils.fromFlatString(id);
} catch (IllegalArgumentException ex) {
GlowServer.logger.log(Level.SEVERE, "Returned authentication UUID invalid: " + id, ex);
session.disconnect("Invalid UUID.");
return;
}
final JSONArray propsArray = (JSONArray) json.get("properties");
// parse properties
final List<PlayerProperty> properties = new ArrayList<>(propsArray.size());
for (Object obj : propsArray) {
JSONObject propJson = (JSONObject) obj;
String propName = (String) propJson.get("name");
String value = (String) propJson.get("value");
String signature = (String) propJson.get("signature");
properties.add(new PlayerProperty(propName, value, signature));
}
final AsyncPlayerPreLoginEvent event = EventFactory.onPlayerPreLogin(name, session.getAddress(), uuid);
if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
session.disconnect(event.getKickMessage(), true);
return;
}
// spawn player
session.getServer().getScheduler().runTask(null, new Runnable() {
@Override
public void run() {
session.setPlayer(new PlayerProfile(name, uuid, properties));
}
});
} catch (Exception e) {
GlowServer.logger.log(Level.SEVERE, "Error in authentication thread", e);
session.disconnect("Internal error during authentication.", true);
}
}
}
}