// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.net.client; import engine.net.AbstractConnection; import engine.server.MBServerStatics; import engine.util.ByteUtils; import org.pmw.tinylog.Logger; import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.ShortBufferException; import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.DHPublicKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.math.BigInteger; import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.security.*; public class ClientAuthenticator { private final AbstractConnection origin; private ByteBuffer buffer = ByteBuffer.allocate(100); private byte[] secretKeyBytes = new byte[16]; private SecretKeySpec BFKey; private Cipher cipher; private byte[] iVecEnc = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; private byte[] iVecDec = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; private int iVecEncOffset = 0; private int iVecDecOffset = 0; private int totalRead = 0; private KeyFactory keyFactory; private DHParameterSpec dhParamSpec; private KeyPairGenerator keyPairGen; private KeyAgreement keyAgree; private boolean initialized = false; private boolean keyInit = false; private byte[] secretKey; private byte[] serverPublicKey; public ClientAuthenticator(AbstractConnection origin) { super(); this.origin = origin; try { // init the resuable Crypto Stuff. this.keyFactory = KeyFactory.getInstance("DH"); this.dhParamSpec = new DHParameterSpec(ClientAuthenticator.P, ClientAuthenticator.G); this.keyPairGen = KeyPairGenerator.getInstance("DH"); this.keyPairGen.initialize(this.dhParamSpec); this.keyAgree = KeyAgreement.getInstance("DH"); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { Logger.error("NoSuchAlgorithmException " + e.getMessage()); this.keyInit = false; return; } this.keyInit = true; } private void calcKeys(AbstractConnection origin, byte[] clientPublicKeyBytes) { try { // get the forwarded client public key, in byte[] form. // Convert client public key to a BigInteger BigInteger clientPublicKeyBI = new BigInteger(1, clientPublicKeyBytes); // convert the client's Public Key to a DHPublicKey object DHPublicKeySpec dhKeySpec = new DHPublicKeySpec(clientPublicKeyBI, ClientAuthenticator.P, ClientAuthenticator.G); DHPublicKey clientPublicKey = (DHPublicKey) this.keyFactory.generatePublic(dhKeySpec); // Now calculate the server's PublicKey byte[] serverPublicKeyBytes = new byte[96]; boolean invalid = true; int tryCnt = 1; while (invalid) { KeyPair keyPair = keyPairGen.generateKeyPair(); this.keyAgree = KeyAgreement.getInstance("DH"); this.keyAgree.init(keyPair.getPrivate()); DHPublicKey serverPublicKey = (DHPublicKey) keyPair.getPublic(); String hex = serverPublicKey.getY().toString(16); if (hex.length() == 192) { invalid = false; serverPublicKeyBytes = ByteUtils.stringHexToByteArray(hex); } if (tryCnt >= 5) // Give java 4 tries to get a Public key of valid length. throw new Exception("Not able to generate a valid length public key"); ++tryCnt; } // Calculate shared DH Secret Key keyAgree.doPhase(clientPublicKey, true); this.secretKey = keyAgree.generateSecret(); this.serverPublicKey = serverPublicKeyBytes; // Now we have the server's publicKey and the common secretKey } catch (Exception e) { origin.disconnect(); Logger.error(e); } } public synchronized int initialize(AbstractConnection origin) { long startTime = System.currentTimeMillis(); int read = -1; // Read the data from connections socket channel try { read = origin.getSocketChannel().read(this.buffer); } catch (IOException e) { if (e.getLocalizedMessage() != null && !e.getLocalizedMessage().equals(MBServerStatics.EXISTING_CONNECTION_CLOSED) && !e.getLocalizedMessage().equals(MBServerStatics.RESET_BY_PEER)) Logger.error(e); origin.disconnect(); return 0; } if (read == -1) { Logger.info("EOF on Socket Channel, Disconnecting " + origin.getLocalAddressAndPortAsString()); origin.disconnect(); return read; } this.totalRead += read; if (this.totalRead > 100) Logger.error( "Possible Spam warning: " + origin.getSocketChannel().socket().toString()); // Not all arrived yet, so wait for more if (this.totalRead < 100) return read; this.buffer.flip(); this.buffer.getInt(); // get the length first & throw away value. byte[] PeerPubKeyEnc = new byte[96]; this.buffer.get(PeerPubKeyEnc); this.calcKeys(origin, PeerPubKeyEnc); try { byte[] sharedSecret = this.secretKey; byte[] PubKeyEnc = this.serverPublicKey; // Cut DH SecretKey down to 16 bytes System.arraycopy(sharedSecret, 0, secretKeyBytes, 0, 16); // Calculate Blowfish Secret Key and make ciphers streams. this.BFKey = new SecretKeySpec(this.secretKeyBytes, "Blowfish"); // Initialize cipher and Ivecs. // Ivecs must be run through the cipher once // to prep the cipher for cfb mode. this.cipher = Cipher.getInstance("Blowfish/ECB/NoPadding"); this.cipher.init(Cipher.ENCRYPT_MODE, this.BFKey); this.cipher.update(this.iVecEnc, 0, 8, this.iVecEnc, 0); this.cipher.update(this.iVecDec, 0, 8, this.iVecDec, 0); // Send public key to peer byte[] pubKeyLen = {0x00, 0x00, 0x00, 0x60}; // hex for 96 ByteBuffer bb = ByteBuffer.wrap(pubKeyLen); bb.position(bb.limit()); origin.sendBB(bb); bb = ByteBuffer.wrap(PubKeyEnc); bb.position(bb.limit()); origin.sendBB(bb); } catch (Exception e) { Logger.error(e); origin.disconnect(); return read; } /* * //Send Secret Key to Login Server if (PortalServer.ServerType == * "Login") { byte[] keyinfo = new byte[20]; keyinfo[0] = 0x11; * keyinfo[1] = 0x11; keyinfo[2] = 0x11; keyinfo[3] = 0x11; for (int i = * 0; i < 16; i++) keyinfo[i + 4] = ShortSharedSecret[i]; * PortalPair.HandleOutput(keyinfo, 20); } */ this.initialized = true; // long endTime = System.currentTimeMillis(); // // long time = endTime - startTime; // // Logger.debug("", "Authenticator took " + time + " ms to initialize."); return read; } public synchronized void encrypt(final ByteBuffer dataIn, final ByteBuffer dataOut) { try { // Assume that if position != 0 that we need to flip. if (dataIn.position() != 0) dataIn.flip(); int count = dataIn.limit(); //Line up the iVecEncOffset.. fall through is intentional if (iVecEncOffset != 0) if ((this.iVecEncOffset + dataIn.limit()) < 8) { //This handles cases where the net msg + offset won't reach 8 bytes total //prevents BufferUnderflowException in small net messages. - int newEncOffset = this.iVecEncOffset + dataIn.limit(); for (int i = this.iVecEncOffset; i < newEncOffset; i++) { this.iVecEnc[i] = (byte) (dataIn.get() ^ this.iVecEnc[i]); dataOut.put(this.iVecEnc[i]); } this.iVecEncOffset = newEncOffset; return; } else switch (iVecEncOffset) { case 1: this.iVecEnc[1] = (byte) (dataIn.get() ^ this.iVecEnc[1]); dataOut.put(this.iVecEnc[1]); case 2: this.iVecEnc[2] = (byte) (dataIn.get() ^ this.iVecEnc[2]); dataOut.put(this.iVecEnc[2]); case 3: this.iVecEnc[3] = (byte) (dataIn.get() ^ this.iVecEnc[3]); dataOut.put(this.iVecEnc[3]); case 4: this.iVecEnc[4] = (byte) (dataIn.get() ^ this.iVecEnc[4]); dataOut.put(this.iVecEnc[4]); case 5: this.iVecEnc[5] = (byte) (dataIn.get() ^ this.iVecEnc[5]); dataOut.put(this.iVecEnc[5]); case 6: this.iVecEnc[6] = (byte) (dataIn.get() ^ this.iVecEnc[6]); dataOut.put(this.iVecEnc[6]); case 7: this.iVecEnc[7] = (byte) (dataIn.get() ^ this.iVecEnc[7]); dataOut.put(this.iVecEnc[7]); count -= (8 - iVecEncOffset); this.iVecEncOffset = 0; this.cipher.update(this.iVecEnc, 0, 8, this.iVecEnc, 0); } //Main loop - unrolled x8 int loopCount = (count) >> 3; for (int i = 0; i < loopCount; i++) { this.iVecEnc[0] = (byte) (dataIn.get() ^ this.iVecEnc[0]); this.iVecEnc[1] = (byte) (dataIn.get() ^ this.iVecEnc[1]); this.iVecEnc[2] = (byte) (dataIn.get() ^ this.iVecEnc[2]); this.iVecEnc[3] = (byte) (dataIn.get() ^ this.iVecEnc[3]); this.iVecEnc[4] = (byte) (dataIn.get() ^ this.iVecEnc[4]); this.iVecEnc[5] = (byte) (dataIn.get() ^ this.iVecEnc[5]); this.iVecEnc[6] = (byte) (dataIn.get() ^ this.iVecEnc[6]); this.iVecEnc[7] = (byte) (dataIn.get() ^ this.iVecEnc[7]); dataOut.put(this.iVecEnc[0]); dataOut.put(this.iVecEnc[1]); dataOut.put(this.iVecEnc[2]); dataOut.put(this.iVecEnc[3]); dataOut.put(this.iVecEnc[4]); dataOut.put(this.iVecEnc[5]); dataOut.put(this.iVecEnc[6]); dataOut.put(this.iVecEnc[7]); this.cipher.update(this.iVecEnc, 0, 8, this.iVecEnc, 0); } //Resync the iVecEncOffset to handle the remainder.. this.iVecEncOffset = count % 8; for (int i = 0; i < iVecEncOffset; i++) { this.iVecEnc[i] = (byte) (dataIn.get() ^ this.iVecEnc[i]); dataOut.put(this.iVecEnc[i]); } } catch (BufferUnderflowException e) { if (dataIn != null && dataOut != null) Logger.warn("Encrypt Error: (in)" + dataIn.toString() + " :: (out)" + dataOut.toString()); Logger.error("ClientAuth.encrypt() -> Underflow" + e); } catch (BufferOverflowException e) { if (dataIn != null && dataOut != null) Logger.warn("Encrypt Error: (in)" + dataIn.toString() + " :: (out)" + dataOut.toString()); Logger.error("ClientAuth.encrypt() -> Overflow" + e); } catch (Exception e) { Logger.error("ClientAuth.encrypt()" + e); } } public synchronized void decrypt(ByteBuffer dataIn, ByteBuffer dataOut) { try { // get lock synchronized (dataIn) { // TODO is this lock needed? // Assume that if position != 0 that we need to flip. if (dataIn.position() != 0) dataIn.flip(); byte encryptedByte; byte decryptedByte; for (int i = 0; i < dataIn.limit(); ++i) { // Get byte out of ByteBuffer encryptedByte = dataIn.get(); // XOR it against the iVEC decryptedByte = (byte) (encryptedByte ^ this.iVecDec[this.iVecDecOffset]); // put the decrypted byte into the outgoing ByteBuffer dataOut.put(decryptedByte); // store the encrypted byte back into the iVEC this.iVecDec[this.iVecDecOffset] = encryptedByte; // Increment ivecOffset this.iVecDecOffset++; // Check to see if iVecOffset is at MAX. If so, reset if (this.iVecDecOffset > 7) { try { this.cipher.update(this.iVecDec, 0, 8, this.iVecDec, 0); } catch (ShortBufferException e) { // suck up this error Logger.error(e); } this.iVecDecOffset = 0; } } } } catch (Exception e) { Logger.error("ClientAuth.decrypt()" + e); } } private void initiateKey(byte[] clientPublicKeyBytes) { } /** * @return the secretKeyBytes */ public byte[] getSecretKeyBytes() { return secretKeyBytes; } /** * @return the initialized */ public boolean initialized() { return initialized; } private static final byte P_Bytes[] = {(byte) 0xFB, (byte) 0x46, (byte) 0x56, (byte) 0xB4, (byte) 0xBE, (byte) 0x81, (byte) 0xA4, (byte) 0x2C, (byte) 0x37, (byte) 0xC4, (byte) 0xA2, (byte) 0x61, (byte) 0x4A, (byte) 0xAC, (byte) 0x65, (byte) 0x90, (byte) 0x31, (byte) 0xB6, (byte) 0x83, (byte) 0x26, (byte) 0x63, (byte) 0x94, (byte) 0x08, (byte) 0x95, (byte) 0x56, (byte) 0x8D, (byte) 0x5E, (byte) 0xBF, (byte) 0x94, (byte) 0x10, (byte) 0x5A, (byte) 0x37, (byte) 0xB6, (byte) 0x82, (byte) 0x1A, (byte) 0x75, (byte) 0x2B, (byte) 0xF1, (byte) 0x94, (byte) 0xB7, (byte) 0x7E, (byte) 0x56, (byte) 0xC6, (byte) 0xD1, (byte) 0xF5, (byte) 0x18, (byte) 0xE1, (byte) 0xA5, (byte) 0x13, (byte) 0x9E, (byte) 0xC1, (byte) 0x85, (byte) 0x98, (byte) 0xB7, (byte) 0x32, (byte) 0xDB, (byte) 0x38, (byte) 0x09, (byte) 0x1A, (byte) 0xF8, (byte) 0x5C, (byte) 0xDA, (byte) 0x4F, (byte) 0x9F, (byte) 0x67, (byte) 0x93, (byte) 0x72, (byte) 0x8F, (byte) 0x75, (byte) 0x4F, (byte) 0x0B, (byte) 0xBD, (byte) 0x69, (byte) 0x61, (byte) 0x97, (byte) 0x1F, (byte) 0xEE, (byte) 0xFB, (byte) 0x5B, (byte) 0xB0, (byte) 0x85, (byte) 0xC4, (byte) 0x27, (byte) 0x7E, (byte) 0x41, (byte) 0x42, (byte) 0xC2, (byte) 0xF1, (byte) 0xDA, (byte) 0x64, (byte) 0x8F, (byte) 0x4E, (byte) 0x28, (byte) 0xFD, (byte) 0x2A, (byte) 0x63}; private static final BigInteger P = new BigInteger(1, P_Bytes); private static final BigInteger G = BigInteger.valueOf(5); }