diff --git a/client/src/main/java/client/Client.java b/client/src/main/java/client/Client.java index 6829399..fb7390c 100755 --- a/client/src/main/java/client/Client.java +++ b/client/src/main/java/client/Client.java @@ -41,6 +41,7 @@ import client.audio.Volume; import client.gui.FileCallback; import client.gui.Font; import client.gui.Gui; +import client.gui.GuiConnect.ServerInfo; import client.gui.GuiConsole; import client.gui.GuiInfo; import client.gui.GuiLoading; @@ -513,14 +514,14 @@ public class Client implements IThreadListener { return networkmanager; } - public void connect(String address, int port, String user, String pass, String access) { - this.displayGuiScreen(GuiLoading.makeWaitTask("Verbinde zu " + (address == null ? "localhost" : address) + ":" + port + " ...")); - Log.NETWORK.info("Verbinde zu " + (address == null ? "localhost" : address) + ":" + port); + public void connect(ServerInfo server) { + this.displayGuiScreen(GuiLoading.makeWaitTask("Verbinde zu " + (server.getAddress() == null ? "localhost" : server.getAddress()) + ":" + server.getPort() + " ...")); + Log.NETWORK.info("Verbinde zu " + (server.getAddress() == null ? "localhost" : server.getAddress()) + ":" + server.getPort()); final NetConnection connection; try { - connection = createNetworkManagerAndConnect(address == null ? InetAddress.getLoopbackAddress() : InetAddress.getByName(IDN.toASCII(address)), port); - connection.setNetHandler(new ClientLoginHandler(connection, this, user, access, pass)); + connection = createNetworkManagerAndConnect(server.getAddress() == null ? InetAddress.getLoopbackAddress() : InetAddress.getByName(IDN.toASCII(server.getAddress())), server.getPort()); + connection.setNetHandler(new ClientLoginHandler(connection, this, server)); connection.sendPacket(new HPacketHandshake(Util.PROTOCOL), new GenericFutureListener>() { public void operationComplete(Future u) throws Exception { connection.setConnectionState(PacketRegistry.LOGIN); @@ -530,7 +531,7 @@ public class Client implements IThreadListener { catch (UnknownHostException u) { Log.NETWORK.error(u, "Konnte nicht zu Server verbinden"); - this.disconnected("Unbekannter Hostname: " + (address == null ? "localhost" : address)); + this.disconnected("Unbekannter Hostname: " + (server.getAddress() == null ? "localhost" : server.getAddress())); return; } catch (Exception e) diff --git a/client/src/main/java/client/gui/GuiConfirm.java b/client/src/main/java/client/gui/GuiConfirm.java index 901117c..5439053 100755 --- a/client/src/main/java/client/gui/GuiConfirm.java +++ b/client/src/main/java/client/gui/GuiConfirm.java @@ -28,10 +28,10 @@ public class GuiConfirm extends Gui implements ButtonCallback { } public void init(int width, int height) { - this.add(new Label(0, 0, 500, 24, this.messageLine1, true)); - this.add(new TransparentArea(0, 80, 500, 300, this.messageLine2, this.gm.world != null && !this.gm.charEditor)); - this.confirmBtn = this.add(new ActButton(48, 500, 200, 24, this, this.confirmButtonText)); - this.cancelBtn = this.add(new ActButton(252, 500, 200, 24, this, this.cancelButtonText)); + this.add(new Label(0, 0, 700, 24, this.messageLine1)); + this.add(new TransparentArea(0, 80, 700, 300, this.messageLine2, this.gm.world != null && !this.gm.charEditor)); + this.confirmBtn = this.add(new ActButton(100, 500, 245, 24, this, this.confirmButtonText)); + this.cancelBtn = this.add(new ActButton(355, 500, 245, 24, this, this.cancelButtonText)); this.shift(); } diff --git a/client/src/main/java/client/gui/GuiConnect.java b/client/src/main/java/client/gui/GuiConnect.java index 961d727..07ad72f 100644 --- a/client/src/main/java/client/gui/GuiConnect.java +++ b/client/src/main/java/client/gui/GuiConnect.java @@ -1,6 +1,9 @@ package client.gui; import java.io.File; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; @@ -16,27 +19,52 @@ import client.util.FileUtils; import common.color.TextColor; import common.log.Log; import common.network.IPlayer; +import common.util.EncryptUtil; import common.util.Tuple; import common.util.Util; public class GuiConnect extends GuiList implements ButtonCallback { public class ServerInfo implements Comparable, ListEntry { + private final boolean direct; + private String name; private String address; private int port; private String user; private String password; private String access; + private KeyPair keypair; + private PublicKey serverKey; + private boolean enforceEncryption; private long lastConnected; - public ServerInfo(String name, String address, int port, String user, String password, String access, long lastConnected) { + public ServerInfo(String address, int port, String user, String password, String access) { + this.direct = true; + this.name = ""; + this.address = address; + this.port = port; + this.user = user; + this.password = password; + this.access = access; + this.lastConnected = -1L; + } + + public ServerInfo(String name, String address, int port, String user, String password, String access, KeyPair keypair, long lastConnected, PublicKey serverKey, boolean enforceEnc) { + this.direct = false; this.name = name; this.address = address; this.port = port; this.user = user; this.password = password; this.access = access; + this.keypair = keypair; this.lastConnected = lastConnected; + this.serverKey = serverKey; + this.enforceEncryption = enforceEnc; + } + + public boolean isDirect() { + return this.direct; } public String getName() { @@ -63,23 +91,42 @@ public class GuiConnect extends GuiList implements Button return this.access; } + public KeyPair getKeypair() { + return this.keypair; + } + + public PublicKey getServerKey() { + return this.serverKey; + } + + public boolean requiresEncryption() { + return this.enforceEncryption; + } + public long getLastConnected() { return this.lastConnected; } - public void setData(String name, String address, int port, String user, String password, String access) { + public void setData(String name, String address, int port, String user, String password, String access, KeyPair keypair, PublicKey serverKey, boolean encryptReq) { this.name = name; this.address = address; this.port = port; this.user = user; this.password = password; this.access = access; + this.keypair = keypair; + this.serverKey = serverKey; + this.enforceEncryption = encryptReq; } public void setLastConnected() { this.lastConnected = System.currentTimeMillis(); } + public void setServerKey(PublicKey key) { + this.serverKey = key; + } + public int compareTo(ServerInfo comp) { return this.lastConnected < comp.lastConnected ? 1 : (this.lastConnected > comp.lastConnected ? -1 : this.name.compareTo(comp.name)); } @@ -128,20 +175,32 @@ public class GuiConnect extends GuiList implements Button String user = ""; String password = ""; String access = ""; + byte[] key = null; + byte[] pubkey = null; + byte[] serverKey = null; + boolean enforceEnc = false; long time = -1L; for(int z = 0; z <= lines.length; z++) { String line = z == lines.length ? null : lines[z]; if(line == null || (line.startsWith("[") && line.endsWith("]"))) { if(!name.isEmpty() && !address.isEmpty() && !user.isEmpty() && user.length() < IPlayer.MAX_USER_LENGTH && IPlayer.isValidUser(user) && password.length() < IPlayer.MAX_PASS_LENGTH && access.length() < IPlayer.MAX_PASS_LENGTH && address.length() < 128 && name.length() < 128 && - port >= 1024 && port <= 32767 && password.length() >= 8 && access.length() >= 8) - this.elements.add(new ServerInfo(name, address, port, user, password, access, time)); + port >= 1024 && port <= 32767 && (password.length() >= 8 || password.isEmpty()) && (access.length() >= 8 || access.isEmpty())) { + PrivateKey priv = key == null ? null : EncryptUtil.decodePrivateKey(key); + PublicKey pub = pubkey == null ? null : EncryptUtil.decodePublicKey(pubkey); + PublicKey serv = serverKey == null ? null : EncryptUtil.decodePublicKey(serverKey); + this.elements.add(new ServerInfo(name, address, port, user, password, access, priv == null || pub == null ? null : new KeyPair(pub, priv), time, serv, enforceEnc)); + } if(line != null) { address = ""; port = -1; user = ""; password = ""; access = ""; + key = null; + pubkey = null; + serverKey = null; + enforceEnc = false; time = -1L; name = line.substring(1, line.length() - 1); } @@ -168,6 +227,14 @@ public class GuiConnect extends GuiList implements Button } catch(NumberFormatException e) { } + else if(value.first.equals("encryption_enforced")) + enforceEnc = true; + else if(value.first.equals("serverkey")) + serverKey = Util.fromHexString(value.second); + else if(value.first.equals("key")) + key = Util.fromHexString(value.second); + else if(value.first.equals("pubkey")) + pubkey = Util.fromHexString(value.second); } } Collections.sort(this.elements); @@ -202,19 +269,39 @@ public class GuiConnect extends GuiList implements Button this.gm.displayGuiScreen(this); } + public void editServer(ServerInfo server) { + if(!server.isDirect()) + this.save(); + } + + public void connect(String address, int port, String user, String pass, String access) { + this.gm.connect(new ServerInfo(address, port, user, pass, access)); + } + private void save() { try { StringBuilder sb = new StringBuilder(); for(ServerInfo server : this.elements) { if(sb.length() > 0) sb.append("\n"); - sb.append("[" + server.getName() + "]\n"); - sb.append("address " + server.getAddress() + "\n"); - sb.append("port " + server.getPort() + "\n"); - sb.append("user " + server.getUser() + "\n"); - sb.append("password " + server.getPassword() + "\n"); - sb.append("access " + server.getAccess() + "\n"); - sb.append("connected " + server.getLastConnected()); + sb.append("[" + server.getName() + "]"); + sb.append("\naddress " + server.getAddress()); + sb.append("\nport " + server.getPort()); + sb.append("\nuser " + server.getUser()); + if(!server.getPassword().isEmpty()) + sb.append("\npassword " + server.getPassword()); + if(!server.getAccess().isEmpty()) + sb.append("\naccess " + server.getAccess()); + if(server.getKeypair() != null) { + sb.append("\nkey " + Util.getHexString(server.getKeypair().getPrivate().getEncoded())); + sb.append("\npubkey " + Util.getHexString(server.getKeypair().getPublic().getEncoded())); + } + if(server.requiresEncryption()) + sb.append("\nencryption_enforced"); + if(server.getLastConnected() != -1L) + sb.append("\nconnected " + server.getLastConnected()); + if(server.getServerKey() != null) + sb.append("\nserverkey " + Util.getHexString(server.getServerKey().getEncoded())); } FileUtils.write(SERVERS_FILE, sb.toString()); } @@ -246,12 +333,12 @@ public class GuiConnect extends GuiList implements Button ServerInfo server = this.getSelected(); if(server != null) { server.setLastConnected(); - this.gm.connect(server.address, server.port, server.user, server.password, server.access); + this.gm.connect(server); } } else if(button == this.createButton) { this.setSelected(-1); - this.gm.displayGuiScreen(new GuiServer(new ServerInfo("", "", -1, "", "", "", -1L))); + this.gm.displayGuiScreen(new GuiServer(new ServerInfo("", "", -1, "", "", "", null, -1L, null, false))); } else if(button == this.editButton) { ServerInfo server = this.getSelected(); @@ -262,7 +349,7 @@ public class GuiConnect extends GuiList implements Button ServerInfo server = this.getSelected(); if(server != null) { this.setSelected(-1); - this.gm.displayGuiScreen(new GuiServer(new ServerInfo(server.name, server.address, server.port, server.user, server.password, server.access, -1L))); + this.gm.displayGuiScreen(new GuiServer(new ServerInfo(server.name, server.address, server.port, server.user, server.password, server.access, server.keypair, -1L, null, server.enforceEncryption))); } } } diff --git a/client/src/main/java/client/gui/GuiServer.java b/client/src/main/java/client/gui/GuiServer.java index 7ab9f6f..030c7ab 100644 --- a/client/src/main/java/client/gui/GuiServer.java +++ b/client/src/main/java/client/gui/GuiServer.java @@ -1,11 +1,15 @@ package client.gui; +import java.security.KeyPair; +import java.security.PublicKey; + import client.gui.GuiConnect.ServerInfo; import client.gui.element.ActButton; import client.gui.element.ButtonCallback; import client.gui.element.Label; import client.gui.element.NavButton; import client.gui.element.PressType; +import client.gui.element.Toggle; import client.gui.element.FieldAction; import client.gui.element.Field; import client.gui.element.FieldCallback; @@ -13,6 +17,7 @@ import client.vars.CVarCategory; import client.vars.Variable; import common.color.TextColor; import common.network.IPlayer; +import common.util.EncryptUtil; public class GuiServer extends Gui implements FieldCallback { public static final GuiServer INSTANCE = new GuiServer(null); @@ -32,6 +37,11 @@ public class GuiServer extends Gui implements FieldCallback { private Label userLabel; private Label passLabel; private Label accLabel; + private Toggle encToggle; + private ActButton keyButton; + private ActButton resetButton; + private KeyPair keypair; + private PublicKey serverKey; public GuiServer(ServerInfo server) { this.server = server; @@ -57,12 +67,12 @@ public class GuiServer extends Gui implements FieldCallback { this.userBox = this.add(new Field(0, 70, 220, 24, IPlayer.MAX_USER_LENGTH, this, IPlayer.VALID_USER, this.server == null ? this.lastUser : this.server.getUser())); this.passBox = this.add(new Field(0, 120, 480, 24, IPlayer.MAX_PASS_LENGTH, this, this.server == null ? this.lastPass : this.server.getPassword())); this.accBox = this.add(new Field(0, 170, 480, 24, IPlayer.MAX_PASS_LENGTH, this, this.server == null ? this.lastAcc : this.server.getAccess())); - this.add(new ActButton(0, 220, 480, 24, new ButtonCallback() { + this.add(new ActButton(0, this.server == null ? 220 : 370, 480, 24, new ButtonCallback() { public void use(ActButton elem, PressType action) { GuiServer.this.connect(); } }, this.server == null ? "Verbinden" : (this.server.getName().isEmpty() ? "Hinzufügen" : "Übernehmen"))); - this.add(new NavButton(0, 250, 480, 24, this.server != null ? GuiConnect.INSTANCE : GuiMenu.INSTANCE, "Zurück")); + this.add(new NavButton(0, this.server == null ? 250 : 400, 480, 24, this.server != null ? GuiConnect.INSTANCE : GuiMenu.INSTANCE, "Zurück")); if(this.server != null) this.nameLabel = this.add(new Label(0, -70, 410, 20, "Name", true)); this.addrLabel = this.add(new Label(0, 0, 410, 20, "Adresse", true)); @@ -71,6 +81,58 @@ public class GuiServer extends Gui implements FieldCallback { this.userLabel = this.add(new Label(0, 50, 220, 20, "Nutzer", true)); this.passLabel = this.add(new Label(0, 100, 480, 20, "Passwort (mind. 8 Zeichen)", true)); this.accLabel = this.add(new Label(0, 150, 480, 20, "Zugang (mind. 8 Zeichen)", true)); + if(this.server != null) { + this.keyButton = this.add(new ActButton(0, 220, 480, 24, new ButtonCallback() { + public void use(ActButton elem, PressType action) { + if(GuiServer.this.keypair == null) { + GuiServer.this.keypair = EncryptUtil.generateKeyPair(); + GuiServer.this.keyButton.setText("Schlüsselpaar entfernen"); + } + else { + final String name = GuiServer.this.nameBox.getText(); + final String addr = GuiServer.this.addrBox.getText(); + final String port = GuiServer.this.portBox.getText(); + final String pass = GuiServer.this.passBox.getText(); + final String acc = GuiServer.this.accBox.getText(); + final boolean reqEnc = GuiServer.this.encToggle.getValue(); + final KeyPair keys = GuiServer.this.keypair; + final PublicKey key = GuiServer.this.serverKey; + GuiServer.this.gm.displayGuiScreen(new GuiConfirm(new GuiConfirm.Callback() { + public void confirm(boolean confirmed) { + GuiServer.this.gm.displayGuiScreen(GuiServer.this); + GuiServer.this.nameBox.setText(name); + GuiServer.this.addrBox.setText(addr); + GuiServer.this.portBox.setText(port); + GuiServer.this.passBox.setText(pass); + GuiServer.this.accBox.setText(acc); + GuiServer.this.encToggle.setValue(reqEnc); + GuiServer.this.serverKey = key; + GuiServer.this.resetButton.enabled = key != null; + if(confirmed) { + GuiServer.this.keypair = null; + GuiServer.this.keyButton.setText("Neues Schlüsselpaar generieren"); + } + else { + GuiServer.this.keypair = keys; + GuiServer.this.keyButton.setText("Schlüsselpaar entfernen"); + } + } + }, "Schlüsselpaar wirklich entfernen?", "Wenn das Schlüsselpaar gelöscht wird, ist es nicht mehr möglich, sich damit auf dem Server zu identifizieren und sich anzumelden.\nDamit könnte der Zugriff auf den Server unmöglich werden.", "Schlüsselpaar löschen", "Abbrechen")); + } + } + }, (this.keypair = this.server.getKeypair()) == null ? "Neues Schlüsselpaar generieren" : "Schlüsselpaar entfernen")); + this.encToggle = this.add(new Toggle(0, 270, 480, 24, false, this.server.requiresEncryption(), null, "Nur Verschlüsselt verbinden")); + GuiServer.this.serverKey = this.server.getServerKey(); + this.resetButton = this.add(new ActButton(0, 320, 480, 24, new ButtonCallback() { + public void use(ActButton elem, PressType action) { + if(GuiServer.this.serverKey != null) { + GuiServer.this.serverKey = null; + GuiServer.this.resetButton.enabled = false; + } + } + }, "Server-Key zurücksetzen")); + this.resetButton.enabled = this.serverKey != null; + } this.shift(); } @@ -134,10 +196,10 @@ public class GuiServer extends Gui implements FieldCallback { this.lastPass = pass; this.lastAcc = acc; this.gm.setDirty(); - this.gm.connect(addr, port, user, pass, acc); + GuiConnect.INSTANCE.connect(addr, port, user, pass, acc); } else { - this.server.setData(name, addr, port, user, pass, acc); + this.server.setData(name, addr, port, user, pass, acc, this.keypair, this.serverKey, this.encToggle.getValue()); GuiConnect.INSTANCE.applyServer(this.server); } } diff --git a/client/src/main/java/client/gui/element/Toggle.java b/client/src/main/java/client/gui/element/Toggle.java index b020dd5..ea0467d 100644 --- a/client/src/main/java/client/gui/element/Toggle.java +++ b/client/src/main/java/client/gui/element/Toggle.java @@ -34,7 +34,8 @@ public class Toggle extends Element { // this.type = this.value != 0 ? ElemType.TOGGLE_ON : ElemType.TOGGLE_OFF; // gui_update_style(this, 1); // this.r_dirty = true; - this.func.use(this, this.value); + if(this.func != null) + this.func.use(this, this.value); this.formatText(); this.playSound(); } @@ -50,4 +51,15 @@ public class Toggle extends Element { else super.drawBackground(); } + + public void setValue(boolean value) { + if(this.value != value) { + this.value = value; + this.formatText(); + } + } + + public boolean getValue() { + return this.value; + } } diff --git a/client/src/main/java/client/network/ClientLoginHandler.java b/client/src/main/java/client/network/ClientLoginHandler.java index b0c4ed7..aa7719b 100755 --- a/client/src/main/java/client/network/ClientLoginHandler.java +++ b/client/src/main/java/client/network/ClientLoginHandler.java @@ -1,37 +1,55 @@ package client.network; import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; import javax.crypto.SecretKey; import client.Client; +import client.gui.GuiConfirm; +import client.gui.GuiConnect; +import client.gui.GuiConnect.ServerInfo; +import client.gui.GuiLoading; import common.net.util.concurrent.Future; import common.net.util.concurrent.GenericFutureListener; import common.network.IClientLoginHandler; import common.network.NetConnection; import common.network.NetHandler; import common.network.PacketRegistry; -import common.packet.LPacketPasswordResponse; +import common.packet.LPacketChallenge; +import common.packet.LPacketPassword; +import common.packet.LPacketPubkey; +import common.packet.LPacketResponse; import common.packet.LPacketStartEncrypt; +import common.packet.RPacketChallenge; import common.packet.RPacketDisconnect; import common.packet.RPacketEnableCompression; import common.packet.RPacketLoginSuccess; import common.packet.RPacketRequestEncrypt; +import common.packet.RPacketResponse; +import common.packet.RPacketServerConfig; import common.util.EncryptUtil; public class ClientLoginHandler extends NetHandler implements IClientLoginHandler { - private final Client gm; - private final NetConnection networkManager; - private final String user; - private final String access; - private final String password; + private static enum LoginState { + HANDSHAKE, CONFIRMING, CHALLENGE, ENCRYPTED, PROVING, AUTHENTICATING, DONE; + } - public ClientLoginHandler(NetConnection conn, Client gmIn, String userIn, String accessIn, String passwordIn) { - this.networkManager = conn; - this.gm = gmIn; - this.user = userIn; - this.access = accessIn; - this.password = passwordIn; + private static final SecureRandom TOKEN_RNG = new SecureRandom(); + + private final Client gm; + private final NetConnection connection; + private final ServerInfo server; + + private LoginState state = LoginState.HANDSHAKE; + private byte[] token; + + public ClientLoginHandler(NetConnection conn, Client gm, ServerInfo server) { + this.connection = conn; + this.gm = gm; + this.server = server; } public void onDisconnect(String reason) @@ -39,32 +57,131 @@ public class ClientLoginHandler extends NetHandler implements IClientLoginHandle this.gm.disconnected(reason); } - public void handleDisconnect(RPacketDisconnect packetIn) + public void handleDisconnect(RPacketDisconnect packet) { - this.networkManager.closeChannel(packetIn.getReason()); + this.connection.closeChannel(packet.getReason()); } public void handleEncrypt(RPacketRequestEncrypt packet) { - this.networkManager.setConnectionState(PacketRegistry.LOGIN); + if(this.state != LoginState.HANDSHAKE) { + this.connection.closeChannel("Unerwartetes Verschlüsselungs-Paket"); + return; + } + this.connection.setConnectionState(PacketRegistry.LOGIN); final SecretKey secret = EncryptUtil.createNewSharedKey(); - PublicKey pubkey = packet.getKey(); - this.networkManager.sendPacket(new LPacketStartEncrypt(secret, pubkey, packet.getToken()), new GenericFutureListener < Future > () { + final PublicKey pubkey = packet.getKey(); + final byte[] token = packet.getToken(); + if(this.server.getServerKey() == null) { + this.state = LoginState.CONFIRMING; + this.gm.schedule(new Runnable() { + public void run() { + ClientLoginHandler.this.gm.displayGuiScreen(new GuiConfirm(new GuiConfirm.Callback() { + public void confirm(boolean confirmed) { + if(confirmed) { + ClientLoginHandler.this.server.setServerKey(pubkey); + GuiConnect.INSTANCE.editServer(ClientLoginHandler.this.server); + ClientLoginHandler.this.gm.displayGuiScreen( + GuiLoading.makeWaitTask("Verbinde zu " + (ClientLoginHandler.this.server.getAddress() == null ? "localhost" : + ClientLoginHandler.this.server.getAddress()) + ":" + ClientLoginHandler.this.server.getPort() + " ...")); + ClientLoginHandler.this.startEncryption(secret, pubkey, token); + } + else { + ClientLoginHandler.this.connection.closeChannel("Verbindung wurde abgebrochen"); + } + } + }, "Die Identität des Servers ist unbekannt", "Es wurde noch nie mit diesem Server verbunden.\nSoll die Verbindung wirklich fortgesetzt werden?\n\nDer öffentliche Schlüssel des Servers lautet:\n" + Base64.getEncoder().encodeToString(pubkey.getEncoded()), "Verbindung herstellen", "Abbrechen und trennen")); + } + }); + return; + } + else if(!this.server.getServerKey().equals(pubkey)) { + this.connection.closeChannel("Die Identität des Servers hat sich geändert\n\nDer Server hat einen anderen öffentlichen Schlüssel als vorher bekannt war, dies kann bedeuten dass der Inhaber jetzt einen anderen Schlüssel verwendet oder sich ein anderer Server als dieser ausgibt (man-in-the-middle attack).\n\nDer öffentliche Schlüssel des Servers lautet:\n" + Base64.getEncoder().encodeToString(pubkey.getEncoded()) + "\n\nDer vertrauenswürdige Schlüssel lautet:\n" + Base64.getEncoder().encodeToString(this.server.getServerKey().getEncoded())); + return; + } + this.startEncryption(secret, pubkey, token); + } + + private void startEncryption(SecretKey secret, PublicKey pubkey, byte[] token) { + this.state = LoginState.CHALLENGE; + this.connection.sendPacket(new LPacketStartEncrypt(secret, pubkey, token), new GenericFutureListener < Future > () { public void operationComplete(Future u) throws Exception { - ClientLoginHandler.this.networkManager.startEncryption(secret); - ClientLoginHandler.this.networkManager.sendPacket(new LPacketPasswordResponse(ClientLoginHandler.this.user, ClientLoginHandler.this.access, ClientLoginHandler.this.password)); + ClientLoginHandler.this.connection.startEncryption(secret); + ClientLoginHandler.this.token = new byte[32]; + TOKEN_RNG.nextBytes(ClientLoginHandler.this.token); + ClientLoginHandler.this.connection.sendPacket(new LPacketChallenge(pubkey, ClientLoginHandler.this.token)); } }); } - public void handleLoginSuccess(RPacketLoginSuccess packetIn) + public void handleResponse(RPacketResponse packet) { + if(this.state != LoginState.CHALLENGE) { + this.connection.closeChannel("Unerwartetes Beweis-Paket"); + return; + } + if(!Arrays.equals(this.token, packet.getToken())) { + this.connection.closeChannel("Fehlerhaftes Beweis-Token"); + return; + } + this.state = LoginState.ENCRYPTED; + } + + public void handleConfig(RPacketServerConfig packet) { + if(this.state == LoginState.HANDSHAKE) { + this.connection.setConnectionState(PacketRegistry.LOGIN); + if(this.server.requiresEncryption()) { + this.connection.closeChannel("Der Server unterstützt keine verschlüsselte Verbindung, dies ist in den Servereinstellungen von '" + + this.server.getName() + "' als erforderlich eingestellt, stelle keine ungesicherte Verbindung her."); + return; + } + } + else if(this.state != LoginState.ENCRYPTED) { + this.connection.closeChannel("Unerwartetes Konfigurations-Paket"); + return; + } + boolean auth = packet.isAuthenticating(); + boolean passwordAuth = packet.canUsePassword(); + boolean pubkeyAuth = packet.canUsePubkey() && this.state == LoginState.ENCRYPTED; + if(auth && (!passwordAuth || this.server.getPassword().isEmpty()) && (!pubkeyAuth || this.server.getKeypair() == null)) { + this.connection.closeChannel("Der Server unterstützt keine der vorhandenen Authentifizierungsmethoden\n\nUnterstützt vom Server: " + + (!passwordAuth && !pubkeyAuth ? "Keine" : ((passwordAuth ? "Passwort" : "") + (passwordAuth && pubkeyAuth ? " und " : "") + (pubkeyAuth ? "Pubkey" : ""))) + + "\n\nVorhanden in Konfiguration für '" + this.server.getName() + "': " + (this.server.getPassword().isEmpty() && this.server.getKeypair() == null ? "Keine" : + ((this.server.getPassword().isEmpty() ? "Passwort" : "") + + (this.server.getPassword().isEmpty() && this.server.getKeypair() != null ? " und " : "") + (this.server.getKeypair() != null ? "Pubkey" : "")))); + return; + } + if(auth && pubkeyAuth && this.server.getKeypair() != null) { + this.state = LoginState.PROVING; + this.connection.sendPacket(new LPacketPubkey(this.server.getUser(), this.server.getAccess(), this.server.getKeypair().getPublic())); + } + else { + this.state = LoginState.AUTHENTICATING; + this.connection.sendPacket(new LPacketPassword(this.server.getUser(), this.server.getAccess(), auth ? this.server.getPassword() : "")); + } + } + + public void handleChallenge(RPacketChallenge packet) { + if(this.state != LoginState.PROVING) { + this.connection.closeChannel("Unerwartetes Anforderungs-Paket"); + return; + } + this.state = LoginState.AUTHENTICATING; + this.connection.sendPacket(new LPacketResponse(packet.getToken(this.server.getKeypair().getPrivate()))); + } + + public void handleLoginSuccess(RPacketLoginSuccess packet) { - this.gm.debugWorld = packetIn.isDebug(); - this.networkManager.setConnectionState(PacketRegistry.PLAY); - this.networkManager.setNetHandler(new ClientPlayer(this.gm, this.networkManager)); + if(this.state != LoginState.AUTHENTICATING) { + this.connection.closeChannel("Unerwartetes Bestätigungs-Paket"); + return; + } + this.state = LoginState.DONE; + this.gm.debugWorld = packet.isDebug(); + this.connection.setConnectionState(PacketRegistry.PLAY); + this.connection.setNetHandler(new ClientPlayer(this.gm, this.connection)); } - public void handleEnableCompression(RPacketEnableCompression packetIn) + public void handleEnableCompression(RPacketEnableCompression packet) { - this.networkManager.setCompressionTreshold(packetIn.getValue()); + this.connection.setCompressionTreshold(packet.getValue()); } } diff --git a/common/src/main/java/common/init/Config.java b/common/src/main/java/common/init/Config.java index b7d2d19..a3cf49e 100755 --- a/common/src/main/java/common/init/Config.java +++ b/common/src/main/java/common/init/Config.java @@ -333,6 +333,16 @@ public abstract class Config { public static boolean register = true; @Var(name = "signEditing") public static boolean editSigns = true; + @Var(name = "passwordAuthentication") + public static boolean passwordAuth = true; + @Var(name = "pubkeyAuthentication") + public static boolean pubkeyAuth = true; + @Var(name = "requireAccessPassword") + public static boolean accessRequired = true; + @Var(name = "encryption") + public static boolean encrypt = true; + @Var(name = "requireAuthentication") + public static boolean authenticate = true; @Var(name = "keepInventory") public static boolean keepInventory = false; diff --git a/common/src/main/java/common/network/IClientLoginHandler.java b/common/src/main/java/common/network/IClientLoginHandler.java index 8f6de83..706a18b 100644 --- a/common/src/main/java/common/network/IClientLoginHandler.java +++ b/common/src/main/java/common/network/IClientLoginHandler.java @@ -1,13 +1,19 @@ package common.network; +import common.packet.RPacketChallenge; import common.packet.RPacketDisconnect; import common.packet.RPacketEnableCompression; import common.packet.RPacketLoginSuccess; import common.packet.RPacketRequestEncrypt; +import common.packet.RPacketResponse; +import common.packet.RPacketServerConfig; public interface IClientLoginHandler { void handleDisconnect(RPacketDisconnect packet); void handleLoginSuccess(RPacketLoginSuccess packet); void handleEnableCompression(RPacketEnableCompression packet); void handleEncrypt(RPacketRequestEncrypt packet); + void handleConfig(RPacketServerConfig packet); + void handleResponse(RPacketResponse packet); + void handleChallenge(RPacketChallenge packet); } diff --git a/common/src/main/java/common/network/ILoginHandler.java b/common/src/main/java/common/network/ILoginHandler.java index 7cc7ffb..48d654f 100644 --- a/common/src/main/java/common/network/ILoginHandler.java +++ b/common/src/main/java/common/network/ILoginHandler.java @@ -1,9 +1,15 @@ package common.network; -import common.packet.LPacketPasswordResponse; +import common.packet.LPacketChallenge; +import common.packet.LPacketPassword; +import common.packet.LPacketPubkey; +import common.packet.LPacketResponse; import common.packet.LPacketStartEncrypt; public interface ILoginHandler { - void processPasswordResponse(LPacketPasswordResponse packet); void processEncryption(LPacketStartEncrypt packet); + void processPassword(LPacketPassword packet); + void processPubkey(LPacketPubkey packet); + void processResponse(LPacketResponse packet); + void processChallenge(LPacketChallenge packet); } diff --git a/common/src/main/java/common/network/PacketRegistry.java b/common/src/main/java/common/network/PacketRegistry.java index 5d5a8b1..eedfd3a 100755 --- a/common/src/main/java/common/network/PacketRegistry.java +++ b/common/src/main/java/common/network/PacketRegistry.java @@ -22,12 +22,18 @@ import common.packet.CPacketPlayer; import common.packet.CPacketSign; import common.packet.CPacketSkin; import common.packet.HPacketHandshake; -import common.packet.LPacketPasswordResponse; +import common.packet.LPacketChallenge; +import common.packet.LPacketPassword; +import common.packet.LPacketPubkey; +import common.packet.LPacketResponse; import common.packet.LPacketStartEncrypt; +import common.packet.RPacketChallenge; import common.packet.RPacketDisconnect; import common.packet.RPacketEnableCompression; import common.packet.RPacketLoginSuccess; import common.packet.RPacketRequestEncrypt; +import common.packet.RPacketResponse; +import common.packet.RPacketServerConfig; import common.packet.SPacketEntityRelMove; import common.packet.SPacketEntityLook; import common.packet.SPacketEntityLookMove; @@ -101,11 +107,17 @@ public enum PacketRegistry { LOGIN {{ this.server(RPacketDisconnect.class); this.server(RPacketRequestEncrypt.class); + this.server(RPacketResponse.class); + this.server(RPacketChallenge.class); + this.server(RPacketServerConfig.class); this.server(RPacketLoginSuccess.class); this.server(RPacketEnableCompression.class); this.client(LPacketStartEncrypt.class); - this.client(LPacketPasswordResponse.class); + this.client(LPacketPassword.class); + this.client(LPacketChallenge.class); + this.client(LPacketPubkey.class); + this.client(LPacketResponse.class); }}, PLAY {{ this.server(SPacketKeepAlive.class); diff --git a/common/src/main/java/common/packet/LPacketChallenge.java b/common/src/main/java/common/packet/LPacketChallenge.java new file mode 100644 index 0000000..469d87e --- /dev/null +++ b/common/src/main/java/common/packet/LPacketChallenge.java @@ -0,0 +1,37 @@ +package common.packet; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import common.network.ILoginHandler; +import common.network.Packet; +import common.network.PacketBuffer; +import common.util.EncryptUtil; + +public class LPacketChallenge implements Packet { + private byte[] token; + + public LPacketChallenge() { + } + + public LPacketChallenge(PublicKey pubkey, byte[] token) { + this.token = EncryptUtil.encryptData(pubkey, token); + } + + public final void readPacketData(PacketBuffer buf) throws IOException { + this.token = buf.readByteArray(); + } + + public final void writePacketData(PacketBuffer buf) throws IOException { + buf.writeByteArray(this.token); + } + + public void processPacket(ILoginHandler handler) { + handler.processChallenge(this); + } + + public byte[] getToken(PrivateKey key) { + return EncryptUtil.decryptData(key, this.token); + } +} diff --git a/common/src/main/java/common/packet/LPacketPasswordResponse.java b/common/src/main/java/common/packet/LPacketPassword.java similarity index 81% rename from common/src/main/java/common/packet/LPacketPasswordResponse.java rename to common/src/main/java/common/packet/LPacketPassword.java index 60dc63c..5436f32 100755 --- a/common/src/main/java/common/packet/LPacketPasswordResponse.java +++ b/common/src/main/java/common/packet/LPacketPassword.java @@ -7,20 +7,20 @@ import common.network.IPlayer; import common.network.Packet; import common.network.PacketBuffer; -public class LPacketPasswordResponse implements Packet +public class LPacketPassword implements Packet { private String user; private String access; private String password; - public LPacketPasswordResponse() + public LPacketPassword() { } - public LPacketPasswordResponse(String userIn, String accessIn, String passwordIn) + public LPacketPassword(String user, String access, String passwordIn) { - this.user = userIn; - this.access = accessIn; + this.user = user; + this.access = access; this.password = passwordIn; } @@ -49,7 +49,7 @@ public class LPacketPasswordResponse implements Packet */ public void processPacket(ILoginHandler handler) { - handler.processPasswordResponse(this); + handler.processPassword(this); } public String getUser() diff --git a/common/src/main/java/common/packet/LPacketPubkey.java b/common/src/main/java/common/packet/LPacketPubkey.java new file mode 100644 index 0000000..873a247 --- /dev/null +++ b/common/src/main/java/common/packet/LPacketPubkey.java @@ -0,0 +1,53 @@ +package common.packet; + +import java.io.IOException; +import java.security.PublicKey; + +import common.network.ILoginHandler; +import common.network.IPlayer; +import common.network.Packet; +import common.network.PacketBuffer; +import common.util.EncryptUtil; + +public class LPacketPubkey implements Packet { + private String user; + private String access; + private PublicKey key; + + public LPacketPubkey() { + } + + public LPacketPubkey(String user, String access, PublicKey key) { + this.user = user; + this.access = access; + this.key = key; + } + + public final void readPacketData(PacketBuffer buf) throws IOException { + this.user = buf.readString(IPlayer.MAX_USER_LENGTH); + this.access = buf.readString(IPlayer.MAX_PASS_LENGTH); + this.key = EncryptUtil.decodePublicKey(buf.readByteArray()); + } + + public final void writePacketData(PacketBuffer buf) throws IOException { + buf.writeString(this.user); + buf.writeString(this.access); + buf.writeByteArray(this.key.getEncoded()); + } + + public void processPacket(ILoginHandler handler) { + handler.processPubkey(this); + } + + public String getUser() { + return this.user; + } + + public String getAccess() { + return this.access; + } + + public PublicKey getKey() { + return this.key; + } +} diff --git a/common/src/main/java/common/packet/LPacketResponse.java b/common/src/main/java/common/packet/LPacketResponse.java new file mode 100644 index 0000000..7c27903 --- /dev/null +++ b/common/src/main/java/common/packet/LPacketResponse.java @@ -0,0 +1,33 @@ +package common.packet; + +import java.io.IOException; +import common.network.ILoginHandler; +import common.network.Packet; +import common.network.PacketBuffer; + +public class LPacketResponse implements Packet { + private byte[] token = new byte[0]; + + public LPacketResponse() { + } + + public LPacketResponse(byte[] token) { + this.token = token; + } + + public void readPacketData(PacketBuffer buf) throws IOException { + this.token = buf.readByteArray(); + } + + public void writePacketData(PacketBuffer buf) throws IOException { + buf.writeByteArray(this.token); + } + + public void processPacket(ILoginHandler handler) { + handler.processResponse(this); + } + + public byte[] getToken() { + return this.token; + } +} diff --git a/common/src/main/java/common/packet/LPacketStartEncrypt.java b/common/src/main/java/common/packet/LPacketStartEncrypt.java index 3408450..fb66fc0 100644 --- a/common/src/main/java/common/packet/LPacketStartEncrypt.java +++ b/common/src/main/java/common/packet/LPacketStartEncrypt.java @@ -42,6 +42,6 @@ public class LPacketStartEncrypt implements Packet { } public byte[] getToken(PrivateKey key) { - return key == null ? this.token : EncryptUtil.decryptData(key, this.token); + return EncryptUtil.decryptData(key, this.token); } } diff --git a/common/src/main/java/common/packet/RPacketChallenge.java b/common/src/main/java/common/packet/RPacketChallenge.java new file mode 100644 index 0000000..d761d42 --- /dev/null +++ b/common/src/main/java/common/packet/RPacketChallenge.java @@ -0,0 +1,37 @@ +package common.packet; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import common.network.IClientLoginHandler; +import common.network.Packet; +import common.network.PacketBuffer; +import common.util.EncryptUtil; + +public class RPacketChallenge implements Packet { + private byte[] token; + + public RPacketChallenge() { + } + + public RPacketChallenge(PublicKey pubkey, byte[] token) { + this.token = EncryptUtil.encryptData(pubkey, token); + } + + public final void readPacketData(PacketBuffer buf) throws IOException { + this.token = buf.readByteArray(); + } + + public final void writePacketData(PacketBuffer buf) throws IOException { + buf.writeByteArray(this.token); + } + + public void processPacket(IClientLoginHandler handler) { + handler.handleChallenge(this); + } + + public byte[] getToken(PrivateKey key) { + return EncryptUtil.decryptData(key, this.token); + } +} diff --git a/common/src/main/java/common/packet/RPacketResponse.java b/common/src/main/java/common/packet/RPacketResponse.java new file mode 100644 index 0000000..abeac80 --- /dev/null +++ b/common/src/main/java/common/packet/RPacketResponse.java @@ -0,0 +1,33 @@ +package common.packet; + +import java.io.IOException; +import common.network.IClientLoginHandler; +import common.network.Packet; +import common.network.PacketBuffer; + +public class RPacketResponse implements Packet { + private byte[] token = new byte[0]; + + public RPacketResponse() { + } + + public RPacketResponse(byte[] token) { + this.token = token; + } + + public void readPacketData(PacketBuffer buf) throws IOException { + this.token = buf.readByteArray(); + } + + public void writePacketData(PacketBuffer buf) throws IOException { + buf.writeByteArray(this.token); + } + + public void processPacket(IClientLoginHandler handler) { + handler.handleResponse(this); + } + + public byte[] getToken() { + return this.token; + } +} diff --git a/common/src/main/java/common/packet/RPacketServerConfig.java b/common/src/main/java/common/packet/RPacketServerConfig.java new file mode 100644 index 0000000..7aa5992 --- /dev/null +++ b/common/src/main/java/common/packet/RPacketServerConfig.java @@ -0,0 +1,60 @@ +package common.packet; + +import java.io.IOException; +import common.network.IClientLoginHandler; +import common.network.Packet; +import common.network.PacketBuffer; + +public class RPacketServerConfig implements Packet { + private boolean requiresAccess; + private boolean requiresAuth; + private boolean passwordAuth; + private boolean pubkeyAuth; + + public RPacketServerConfig() { + } + + public RPacketServerConfig(boolean requiresAccess, boolean requiresAuth, boolean passwordAuth, boolean pubkeyAuth) { + this.requiresAccess = requiresAccess; + this.requiresAuth = requiresAuth; + this.passwordAuth = passwordAuth; + this.pubkeyAuth = pubkeyAuth; + } + + public final void readPacketData(PacketBuffer buf) throws IOException { + byte flags = buf.readByte(); + this.requiresAccess = (flags & 1) != 0; + this.requiresAuth = (flags & 2) != 0; + this.passwordAuth = (flags & 4) != 0; + this.pubkeyAuth = (flags & 8) != 0; + } + + public final void writePacketData(PacketBuffer buf) throws IOException { + byte flags = 0; + flags |= this.requiresAccess ? 1 : 0; + flags |= this.requiresAuth ? 2 : 0; + flags |= this.passwordAuth ? 4 : 0; + flags |= this.pubkeyAuth ? 8 : 0; + buf.writeByte(flags); + } + + public void processPacket(IClientLoginHandler handler) { + handler.handleConfig(this); + } + + public boolean hasAccessPassword() { + return this.requiresAccess; + } + + public boolean isAuthenticating() { + return this.passwordAuth; + } + + public boolean canUsePassword() { + return this.passwordAuth; + } + + public boolean canUsePubkey() { + return this.pubkeyAuth; + } +} diff --git a/common/src/main/java/common/util/EncryptUtil.java b/common/src/main/java/common/util/EncryptUtil.java index 5241f2e..dba9541 100644 --- a/common/src/main/java/common/util/EncryptUtil.java +++ b/common/src/main/java/common/util/EncryptUtil.java @@ -12,6 +12,7 @@ import java.security.PublicKey; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -59,6 +60,18 @@ public class EncryptUtil { return null; } } + + public static PrivateKey decodePrivateKey(byte[] encoded) { + try { + EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return factory.generatePrivate(spec); + } + catch(NoSuchAlgorithmException | InvalidKeySpecException e) { + Log.SYSTEM.error(e, "Privater Schlüssel konnte nicht dekodiert werden"); + return null; + } + } public static SecretKey decryptSharedKey(PrivateKey key, byte[] secret) { return new SecretKeySpec(decryptData(key, secret), "AES"); diff --git a/common/src/main/java/common/util/Util.java b/common/src/main/java/common/util/Util.java index 230fdfc..6b614ee 100644 --- a/common/src/main/java/common/util/Util.java +++ b/common/src/main/java/common/util/Util.java @@ -408,4 +408,27 @@ int utf_len(const char *str) { } return sb.toString(); } + + public static String getHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for(int z = 0; z < bytes.length; z++) { + sb.append(String.format("%02x", bytes[z])); + } + return sb.toString(); + } + + public static byte[] fromHexString(String str) { + if((str.length() & 1) == 1) + str = "0" + str; + byte[] bytes = new byte[str.length() / 2]; + try { + for(int z = 0; z < bytes.length; z++) { + bytes[z] = (byte)Integer.parseUnsignedInt(str.substring(z * 2, (z + 1) * 2), 16); + } + } + catch(NumberFormatException e) { + return null; + } + return bytes; + } } diff --git a/server/src/main/java/server/Server.java b/server/src/main/java/server/Server.java index ac8a329..7f611c4 100755 --- a/server/src/main/java/server/Server.java +++ b/server/src/main/java/server/Server.java @@ -121,9 +121,9 @@ public final class Server implements IThreadListener { private final List unload = Lists.newArrayList(); private final Map warps = Maps.newTreeMap(); private final CommandEnvironment scriptEnv = new CommandEnvironment(this); - private final KeyPair keyPair; private final boolean debug; + private KeyPair keyPair; private WorldServer space; private ChannelFuture endpoint; @@ -169,7 +169,7 @@ public final class Server implements IThreadListener { Log.flushLog(); } - public static void saveServerConfig(long time) { + public static void saveServerConfig(long time, Server server) { TagObject data = new TagObject(); data.setLong("Time", time); data.setLong("LastAccess", System.currentTimeMillis()); @@ -182,6 +182,10 @@ public final class Server implements IThreadListener { } data.setObject("Config", cfg); data.setObject("Universe", UniverseRegistry.toTags()); + if(server != null) { + data.setByteArray("PrivateKey", server.getPrivateKey().getEncoded()); + data.setByteArray("PublicKey", server.getPublicKey().getEncoded()); + } File nfile = new File("server.cdt.tmp"); File lfile = new File("server.cdt"); try { @@ -195,7 +199,7 @@ public final class Server implements IThreadListener { } } - public static long loadServerConfig() { + public long loadServerConfig() { Config.clear(); UniverseRegistry.clear(); File file = new File("server.cdt"); @@ -216,6 +220,12 @@ public final class Server implements IThreadListener { Log.IO.info("Version: %s", version); Log.IO.info("Weltzeit: %d Ticks / %d Sekunden", time, time / 20L); Log.IO.info("Zuletzt geladen: %s", new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date(lastPlayed))); + if(tag.hasByteArray("PrivateKey") && tag.hasByteArray("PublicKey")) { + PrivateKey key = EncryptUtil.decodePrivateKey(tag.getByteArray("PrivateKey")); + PublicKey pubkey = EncryptUtil.decodePublicKey(tag.getByteArray("PublicKey")); + if(key != null && pubkey != null) + this.keyPair = new KeyPair(pubkey, key); + } return time; } catch(Exception e) { @@ -258,7 +268,6 @@ public final class Server implements IThreadListener { } } }, "password"); - this.keyPair = EncryptUtil.generateKeyPair(); } public CommandEnvironment getScriptEnvironment() { @@ -283,7 +292,7 @@ public final class Server implements IThreadListener { public void saveWorldInfo() { if(!this.debug) { - saveServerConfig(this.space.getDayTime()); + saveServerConfig(this.space.getDayTime(), this); WorldServer.saveWarps(this.warps); } } @@ -452,7 +461,11 @@ public final class Server implements IThreadListener { public void run(long time) { if(!this.debug) { Converter.convert(); - long wtime = loadServerConfig(); + long wtime = this.loadServerConfig(); + if(this.keyPair == null) { + Log.SYSTEM.info("Generiere neues Schlüsselpaar"); + this.keyPair = EncryptUtil.generateKeyPair(); + } // if(dtime == -1L) // { // dtime = World.START_TIME; //// Config.set("spawnDim", "1", null); @@ -485,6 +498,8 @@ public final class Server implements IThreadListener { // } } else { + Log.SYSTEM.info("Generiere temporäres Schlüsselpaar"); + this.keyPair = EncryptUtil.generateKeyPair(); Config.clear(); UniverseRegistry.clear(); Config.set("daylightCycle", "false", false); @@ -831,29 +846,41 @@ public final class Server implements IThreadListener { radius > 0 ? 0.0f : Config.spawnPitch, world.dimension.getDimensionId()); } - public String addPlayer(NetConnection connection, String loginUser, String loginPass) { + public String addPlayer(NetConnection connection, String loginUser, String loginPass, PublicKey loginKey) { TagObject tag = this.readPlayer(loginUser); Player conn = new Player(this, connection, loginUser); if(tag != null) conn.readTags(tag); + if(Config.authenticate) { + if(conn.getPassword() == null && conn.getPubkey() == null) { + if(tag != null) + return loginKey != null ? "Falscher Pubkey" : "Falsches Passwort"; + if(!Config.register) + return "Anmeldung neuer Accounts ist auf diesem Server deaktiviert (Whitelisted)"; + if(Config.playerLimit > 0 && this.players.size() >= Config.playerLimit) + return String.format("Der Server ist voll (%d/%d)!", this.players.size(), Config.playerLimit); + if(loginKey != null) { + conn.setPubkey(loginKey); + Log.NETWORK.info(loginUser + " registrierte sich mit Pubkey"); + } + else { + if(loginPass == null || loginPass.length() == 0) + return "Ein neues Passwort ist erforderlich um diesen Server zu betreten (mindestens " + Config.minPassLength + " Zeichen)"; + if(loginPass.length() < Config.minPassLength) + return "Passwort ist zu kurz, mindestens " + Config.minPassLength + " Zeichen"; + conn.setPassword(loginPass); + Log.NETWORK.info(loginUser + " registrierte sich mit Passwort"); + } + } + else if(conn.getPubkey() != null ? !conn.getPubkey().equals(loginKey) : !conn.getPassword().equals(loginPass)) { + return loginKey != null ? "Falscher Pubkey" : "Falsches Passwort"; + } + else { + Log.NETWORK.info(loginUser + " loggte sich mit " + (loginKey != null ? "Pubkey" : "Passwort") + " ein"); + } + } if(Config.playerLimit > 0 && this.players.size() >= Config.playerLimit && !conn.isAdmin()) return String.format("Der Server ist voll (%d/%d)!", this.players.size(), Config.playerLimit); - if(conn.getPassword() == null) { - if(!Config.register) - return "Anmeldung neuer Accounts ist auf diesem Server deaktiviert (Whitelisted)"; - if(loginPass.length() == 0) - return "Ein neues Passwort ist erforderlich um diesen Server zu betreten (mindestens " + Config.minPassLength + " Zeichen)"; - if(loginPass.length() < Config.minPassLength) - return "Passwort ist zu kurz, mindestens " + Config.minPassLength + " Zeichen"; - conn.setPassword(loginPass); - Log.NETWORK.info(loginUser + " registrierte sich mit Passwort"); - } - else if(!conn.getPassword().equals(loginPass)) { - return "Falsches Passwort"; - } - else { - Log.NETWORK.info(loginUser + " loggte sich mit Passwort ein"); - } if(Config.compression >= 0) { connection.sendPacket(new RPacketEnableCompression(Config.compression), new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { diff --git a/server/src/main/java/server/network/LoginHandler.java b/server/src/main/java/server/network/LoginHandler.java index 5a3b684..b00729a 100755 --- a/server/src/main/java/server/network/LoginHandler.java +++ b/server/src/main/java/server/network/LoginHandler.java @@ -1,6 +1,7 @@ package server.network; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.SecureRandom; import java.util.Arrays; @@ -9,20 +10,28 @@ import javax.crypto.SecretKey; import common.color.TextColor; import common.init.Config; import common.log.Log; +import common.net.util.concurrent.Future; +import common.net.util.concurrent.GenericFutureListener; import common.network.ILoginHandler; import common.network.IPlayer; import common.network.NetConnection; import common.network.NetHandler; -import common.packet.LPacketPasswordResponse; +import common.packet.LPacketChallenge; +import common.packet.LPacketPassword; +import common.packet.LPacketPubkey; +import common.packet.LPacketResponse; import common.packet.LPacketStartEncrypt; +import common.packet.RPacketChallenge; import common.packet.RPacketDisconnect; import common.packet.RPacketRequestEncrypt; +import common.packet.RPacketResponse; +import common.packet.RPacketServerConfig; import server.Server; public class LoginHandler extends NetHandler implements ILoginHandler { private static enum LoginState { - INIT, ENCRYPT, PASSWORD, AUTHENTICATED, ACCEPTED; + INIT, ENCRYPT, PROOF, PASSWORD, CHALLENGE, AUTHENTICATED, ACCEPTED; } private static final SecureRandom TOKEN_RNG = new SecureRandom(); @@ -35,6 +44,7 @@ public class LoginHandler extends NetHandler implements ILoginHandler private String loginUser; private String loginPass; private byte[] loginToken; + private PublicKey loginKey; public LoginHandler(Server server, NetConnection netManager) { @@ -97,7 +107,7 @@ public class LoginHandler extends NetHandler implements ILoginHandler //// player.netHandler.kick("Du hast dich von einen anderen Ort verbunden"); // } // this.networkManager.sendPacket(new RPacketLoginSuccess()); - String kick = this.server.addPlayer(this.netManager, this.loginUser, this.loginPass); + String kick = this.server.addPlayer(this.netManager, this.loginUser, this.loginPass, this.loginKey); if(kick != null) this.closeConnection(kick); else @@ -107,10 +117,16 @@ public class LoginHandler extends NetHandler implements ILoginHandler public void sendLoginPacket() { if(this.state != LoginState.INIT) throw new IllegalStateException("Unerwartetes Handshake-Paket"); - this.state = LoginState.ENCRYPT; - this.loginToken = new byte[4]; - TOKEN_RNG.nextBytes(this.loginToken); - this.netManager.sendPacket(new RPacketRequestEncrypt(this.server.getPublicKey(), this.loginToken)); + if(Config.encrypt) { + this.state = LoginState.ENCRYPT; + this.loginToken = new byte[4]; + TOKEN_RNG.nextBytes(this.loginToken); + this.netManager.sendPacket(new RPacketRequestEncrypt(this.server.getPublicKey(), this.loginToken)); + } + else { + this.state = LoginState.PASSWORD; + this.netManager.sendPacket(new RPacketServerConfig(Config.accessRequired, Config.authenticate, Config.authenticate && Config.passwordAuth, false)); + } } public void processEncryption(LPacketStartEncrypt packet) { @@ -121,26 +137,81 @@ public class LoginHandler extends NetHandler implements ILoginHandler throw new IllegalStateException("Fehlerhaftes Token"); SecretKey key = packet.getKey(pkey); this.netManager.startEncryption(key); + this.state = LoginState.PROOF; + } + + public void processChallenge(LPacketChallenge packet) { + if(this.state != LoginState.PROOF) + throw new IllegalStateException("Unerwartetes Anforderungs-Paket"); this.state = LoginState.PASSWORD; + this.netManager.sendPacket(new RPacketResponse(packet.getToken(this.server.getPrivateKey())), new GenericFutureListener < Future > () { + public void operationComplete(Future u) throws Exception { + LoginHandler.this.netManager.sendPacket(new RPacketServerConfig(Config.accessRequired, Config.authenticate, Config.authenticate && Config.passwordAuth, + Config.authenticate && Config.pubkeyAuth)); + } + }); + } + + private boolean checkAccess(String access) { + if(Config.accessRequired) { + if(Config.password.length() < 8) { + this.closeConnection("Es ist kein Zugangspasswort für diesen Server konfiguriert"); + return false; + } + if(!Config.password.equals(access)) { + this.closeConnection("Falsches Zugangspasswort"); + return false; + } + } + return true; } - public void processPasswordResponse(LPacketPasswordResponse packetIn) { + public void processPassword(LPacketPassword packet) { if(this.state != LoginState.PASSWORD) throw new IllegalStateException("Unerwartetes Passwort-Paket"); - this.loginUser = packetIn.getUser(); - this.loginPass = packetIn.getPassword(); + this.loginUser = packet.getUser(); if(this.loginUser.isEmpty() || !IPlayer.isValidUser(this.loginUser)) throw new IllegalStateException("Ungültiger Nutzername!"); -// if(!this.checkConnect(packetIn.getAccess())) -// return; - if(Config.password.length() < 8) { - this.closeConnection("Es ist kein Zugangspasswort für diesen Server konfiguriert"); - return; + if(!Config.passwordAuth && Config.authenticate) { + this.closeConnection("Dieser Server " + (Config.pubkeyAuth && Config.encrypt ? "benötigt einen öffentlichen Schlüssel zur Authentifizierung" : "hat keine Authentifizierungsmethode konfiguriert")); + return; } - if(!Config.password.equals(packetIn.getAccess())) { - this.closeConnection("Falsches Zugangspasswort"); - return; + if(!this.checkAccess(packet.getAccess())) + return; + if(Config.authenticate) + this.loginPass = packet.getPassword(); + this.state = LoginState.AUTHENTICATED; + } + + public void processPubkey(LPacketPubkey packet) { + if(this.state != LoginState.PASSWORD) + throw new IllegalStateException("Unerwartetes Pubkey-Paket"); + this.loginUser = packet.getUser(); + if(this.loginUser.isEmpty() || !IPlayer.isValidUser(this.loginUser)) + throw new IllegalStateException("Ungültiger Nutzername!"); + if((!Config.pubkeyAuth || !Config.encrypt) && Config.authenticate) { + this.closeConnection("Dieser Server " + (Config.passwordAuth ? "benötigt ein Passwort zur Authentifizierung" : "hat keine Authentifizierungsmethode konfiguriert")); + return; } + if(!this.checkAccess(packet.getAccess())) + return; + if(Config.authenticate) { + this.loginKey = packet.getKey(); + this.loginToken = new byte[32]; + TOKEN_RNG.nextBytes(this.loginToken); + this.netManager.sendPacket(new RPacketChallenge(this.loginKey, this.loginToken)); + this.state = LoginState.CHALLENGE; + } + else { + this.state = LoginState.AUTHENTICATED; + } + } + + public void processResponse(LPacketResponse packet) { + if(this.state != LoginState.CHALLENGE) + throw new IllegalStateException("Unerwartetes Beweis-Paket"); + if(!Arrays.equals(this.loginToken, packet.getToken())) + throw new IllegalStateException("Fehlerhaftes Beweis-Token"); this.state = LoginState.AUTHENTICATED; } } diff --git a/server/src/main/java/server/network/Player.java b/server/src/main/java/server/network/Player.java index 1c79ca3..2e526a1 100755 --- a/server/src/main/java/server/network/Player.java +++ b/server/src/main/java/server/network/Player.java @@ -1,5 +1,6 @@ package server.network; +import java.security.PublicKey; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; @@ -110,6 +111,7 @@ import common.tileentity.TileEntitySign; import common.util.BlockPos; import common.util.BoundingBox; import common.util.ChunkPos; +import common.util.EncryptUtil; import common.util.ExtMath; import common.util.Facing; import common.util.IntHashMap; @@ -171,6 +173,7 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer private int ping; private boolean deleted; private String password; + private PublicKey pubkey; private boolean profiling; private int selectionDim = Integer.MIN_VALUE; @@ -347,6 +350,14 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer public String getPassword() { return this.password; } + + public void setPubkey(PublicKey key) { + this.pubkey = key; + } + + public PublicKey getPubkey() { + return this.pubkey; + } @@ -619,6 +630,8 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer this.admin = tag.getBool("admin"); if(tag.hasString("password")) this.password = tag.getString("password"); + if(tag.hasByteArray("pubkey")) + this.pubkey = EncryptUtil.decodePublicKey(tag.getByteArray("pubkey")); this.selected = tag.getInt("selected"); List list = tag.getList("characters"); for(int z = 0; z < list.size(); z++) { @@ -633,6 +646,8 @@ public class Player extends NetHandler implements ICrafting, Executor, IPlayer tag.setBool("admin", this.admin); if(this.password != null) tag.setString("password", this.password); + if(this.pubkey != null) + tag.setByteArray("pubkey", this.pubkey.getEncoded()); if(!this.characters.isEmpty()) { tag.setInt("selected", this.selected); List list = Lists.newArrayList(); diff --git a/server/src/main/java/server/world/Converter.java b/server/src/main/java/server/world/Converter.java index f236d30..6f1a89d 100644 --- a/server/src/main/java/server/world/Converter.java +++ b/server/src/main/java/server/world/Converter.java @@ -1308,7 +1308,7 @@ public abstract class Converter { Config.set(rule.getValue(), rules.getString(rule.getKey()), false); } Log.IO.info("Speichere neue server.cdt ..."); - Server.saveServerConfig(World.START_TIME); + Server.saveServerConfig(World.START_TIME, null); Weather weather = tag.getByte("thundering") != 0 ? Weather.THUNDER : (tag.getByte("raining") != 0 ? Weather.RAIN : Weather.CLEAR); if(weather != Weather.CLEAR) { TagObject dataTag = new TagObject();