diff --git a/G-Earth/pom.xml b/G-Earth/pom.xml index 36e305f..03f836f 100644 --- a/G-Earth/pom.xml +++ b/G-Earth/pom.xml @@ -345,6 +345,12 @@ 5.10.2 test + + org.mockito + mockito-inline + 4.11.0 + test + diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java index ca3e7f2..611f71e 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/shockwave/ShockwaveProxy.java @@ -9,6 +9,7 @@ import gearth.protocol.connection.HStateSetter; import gearth.protocol.connection.proxy.ProxyProvider; import gearth.protocol.interceptor.ConnectionInterceptor; import gearth.protocol.interceptor.ConnectionInterceptorCallbacks; +import gearth.protocol.memory.Rc4Obtainer; import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.shockwave.ShockwavePacketIncomingHandler; import gearth.protocol.packethandler.shockwave.ShockwavePacketOutgoingHandler; @@ -92,9 +93,12 @@ public class ShockwaveProxy implements ProxyProvider, ConnectionInterceptorCallb final ShockwavePacketOutgoingHandler outgoingHandler = new ShockwavePacketOutgoingHandler(server.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables()); final ShockwavePacketIncomingHandler incomingHandler = new ShockwavePacketIncomingHandler(client.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables(), outgoingHandler); - // TODO: Non hardcoded version "20". Not exactly sure yet how to deal with this for now. + final Rc4Obtainer rc4Obtainer = new Rc4Obtainer(hConnection); + + rc4Obtainer.setFlashPacketHandlers(outgoingHandler, incomingHandler); + // TODO: Non hardcoded version "24". Not exactly sure yet how to deal with this for now. // Lets revisit when origins is more mature. - proxy.verifyProxy(incomingHandler, outgoingHandler, "20", "SHOCKWAVE"); + proxy.verifyProxy(incomingHandler, outgoingHandler, "24", "SHOCKWAVE"); hProxySetter.setProxy(proxy); onConnect(); @@ -107,7 +111,6 @@ public class ShockwaveProxy implements ProxyProvider, ConnectionInterceptorCallb try { if (!server.isClosed()) server.close(); if (!client.isClosed()) client.close(); - if (HConnection.DEBUG) System.out.println("STOP"); onConnectEnd(); } catch (IOException e) { logger.error("Error occurred while closing sockets.", e); diff --git a/G-Earth/src/main/java/gearth/protocol/crypto/RC4.java b/G-Earth/src/main/java/gearth/protocol/crypto/RC4.java index 4e51cbe..91a88ab 100644 --- a/G-Earth/src/main/java/gearth/protocol/crypto/RC4.java +++ b/G-Earth/src/main/java/gearth/protocol/crypto/RC4.java @@ -54,9 +54,9 @@ import java.util.Arrays; *

* @author Clarence Ho */ -public class RC4 { +public class RC4 implements RC4Cipher { - private byte state[] = new byte[256]; + private byte[] state = new byte[256]; private int x; private int y; @@ -79,9 +79,8 @@ public class RC4 { * @param key the encryption/decryption key */ public RC4(byte[] key) throws NullPointerException { - - for (int i=0; i < 256; i++) { - state[i] = (byte)i; + for (int i = 0; i < 256; i++) { + state[i] = (byte) i; } x = 0; @@ -96,8 +95,7 @@ public class RC4 { throw new NullPointerException(); } - for (int i=0; i < 256; i++) { - + for (int i = 0; i < 256; i++) { index2 = ((key[index1] & 0xff) + (state[i] & 0xff) + index2) & 0xff; tmp = state[i]; @@ -106,9 +104,6 @@ public class RC4 { index1 = (index1 + 1) % key.length; } - - - } public RC4(byte[] state, int x, int y) { @@ -117,51 +112,29 @@ public class RC4 { this.state = state; } - //copyconstructor - public RC4 deepCopy() { - return new RC4(Arrays.copyOf(state, 256), x, y); - } - /** * RC4 encryption/decryption. * - * @param data the data to be encrypted/decrypted + * @param data the data to be encrypted/decrypted * @return the result of the encryption/decryption */ - public byte[] rc4(String data) { + @Override + public byte[] cipher(byte[] data) { + return cipher(data, 0, data.length); + } + + @Override + public byte[] cipher(byte[] data, int offset, int length) { + int xorIndex; + byte tmp; if (data == null) { return null; } - byte[] tmp = data.getBytes(); + byte[] result = new byte[length]; - this.rc4(tmp); - - return tmp; - } - - /** - * RC4 encryption/decryption. - * - * @param buf the data to be encrypted/decrypted - * @return the result of the encryption/decryption - */ - public byte[] rc4(byte[] buf) { - - //int lx = this.x; - //int ly = this.y; - - int xorIndex; - byte tmp; - - if (buf == null) { - return null; - } - - byte[] result = new byte[buf.length]; - - for (int i=0; i < buf.length; i++) { + for (int i = 0; i < length; i++) { x = (x + 1) & 0xff; y = ((state[x] & 0xff) + y) & 0xff; @@ -170,26 +143,50 @@ public class RC4 { state[x] = state[y]; state[y] = tmp; - xorIndex = ((state[x] &0xff) + (state[y] & 0xff)) & 0xff; - result[i] = (byte)(buf[i] ^ state[xorIndex]); + xorIndex = ((state[x] & 0xff) + (state[y] & 0xff)) & 0xff; + result[i] = (byte) (data[offset + i] ^ state[xorIndex]); } - //this.x = lx; - //this.y = ly; - return result; } + @Override + public byte[] decipher(byte[] data) { + return cipher(data); + } + + @Override + public byte[] decipher(byte[] data, int offset, int length) { + return cipher(data, offset, length); + } + + @Override + public byte[] getState () { + return state; + } + + @Override + public int getQ() { + return x; + } + + @Override + public int getJ() { + return y; + } + + public RC4 deepCopy() { + return new RC4(Arrays.copyOf(state, 256), x, y); + } + public boolean couldBeFresh() { return (x == 0 && y == 0); } public void undoRc4(byte[] buf) { - byte tmp; - for (int i = buf.length - 1; i >= 0; i--) { - + for (int i = 0; i < buf.length; i++) { tmp = state[x]; state[x] = state[y]; state[y] = tmp; @@ -197,10 +194,5 @@ public class RC4 { y = (y - (state[x] & 0xff)) & 0xff; x = (x - 1) & 0xff; } - - } - - public byte[] getState () { - return state; } } \ No newline at end of file diff --git a/G-Earth/src/main/java/gearth/protocol/crypto/RC4Base64.java b/G-Earth/src/main/java/gearth/protocol/crypto/RC4Base64.java new file mode 100644 index 0000000..15b206d --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/crypto/RC4Base64.java @@ -0,0 +1,195 @@ +package gearth.protocol.crypto; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Non-standard RC4 algorithm using base64. + * Thanks to Joni Aromaa for the original implementation. + * + * https://github.com/aromaa/Skylight3/blob/72ec3a07d126de09f6de4251c91001329f77a8a2/src/Skylight.Server/Net/Crypto/RC4Base64.cs + * + */ +public class RC4Base64 implements RC4Cipher { + + private static final byte[] BASE64_ENCODING_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] BASE64_DECODING_MAP = new byte[]{ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + private final byte[] state; + private int q; + private int j; + + public RC4Base64(byte[] state, int q, int j) { + if (state.length != 256) { + throw new IllegalArgumentException(String.format("State must be 256 bytes long, was %d", state.length)); + } + + this.q = q; + this.j = j; + this.state = state; + } + + @Override + public byte[] cipher(byte[] data) { + return cipher(data, 0, data.length); + } + + @Override + public byte[] cipher(byte[] data, int offset, int length) { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + + for (int i = 0; i < length; i += 3) { + int firstByte = data[offset + i] ^ moveUp(); + int secondByte = data.length > i + 1 ? (data[i + 1] ^ moveUp()) : 0; + + result.write(BASE64_ENCODING_MAP[(firstByte & 0xFC) >> 2]); + result.write(BASE64_ENCODING_MAP[((firstByte & 0x03) << 4) | ((secondByte & 0xF0) >> 4)]); + + if (data.length > i + 1) { + int thirdByte = data.length > i + 2 ? (data[i + 2] ^ moveUp()) : 0; + + result.write(BASE64_ENCODING_MAP[((secondByte & 0x0F) << 2) | ((thirdByte & 0xC0) >> 6)]); + + if (data.length > i + 2) { + result.write(BASE64_ENCODING_MAP[thirdByte & 0x3F]); + } + } + } + + return result.toByteArray(); + } + + @Override + public byte[] decipher(byte[] data) { + return decipher(data, 0, data.length); + } + + @Override + public byte[] decipher(byte[] data, int offset, int length) { + final ByteBuffer buffer = ByteBuffer.wrap(data, offset, length); + final ByteArrayOutputStream resultBuffer = new ByteArrayOutputStream(); + + while (buffer.hasRemaining()) { + int firstByte = BASE64_DECODING_MAP[buffer.get()]; + int secondByte = BASE64_DECODING_MAP[buffer.get()]; + + int byte1a = firstByte << 2; + int byte1b = (secondByte & 0x30) >> 4; + + resultBuffer.write((byte) ((byte1a | byte1b) ^ moveUp())); + + if (buffer.hasRemaining()) { + int thirdByte = BASE64_DECODING_MAP[buffer.get()]; + + int byte2a = (secondByte & 0x0F) << 4; + int byte2b = (thirdByte & 0x3C) >> 2; + + resultBuffer.write((byte) ((byte2a | byte2b) ^ moveUp())); + + if (buffer.hasRemaining()) { + int fourthByte = BASE64_DECODING_MAP[buffer.get()]; + + int byte3a = (thirdByte & 0x03) << 6; + int byte3b = fourthByte & 0x3F; + + resultBuffer.write((byte) ((byte3a | byte3b) ^ moveUp())); + } + } + } + + return resultBuffer.toByteArray(); + } + + @Override + public byte[] getState () { + return state; + } + + @Override + public int getQ() { + return q; + } + + @Override + public int getJ() { + return j; + } + + @Override + public RC4Base64 deepCopy() { + return new RC4Base64(Arrays.copyOf(state, 256), q, j); + } + + public byte moveUp() { + q = (q + 1) & 0xff; + j = ((state[q] & 0xff) + j) & 0xff; + + byte tmp = state[q]; + state[q] = state[j]; + state[j] = tmp; + + if ((q & 0x3F) == 0x3F) { + int x2 = 297 * (q + 67) & 0xff; + int y2 = (j + state[x2]) & 0xff; + + tmp = state[x2]; + state[x2] = state[y2]; + state[y2] = tmp; + } + + int xorIndex = ((state[q] &0xff) + (state[j] & 0xff)) & 0xff; + + return state[xorIndex]; + } + + public boolean moveDown() { + byte tmp; + + if ((q & 0x3F) == 0x3F) { + // Unsupported. + return false; + } + + tmp = state[q]; + state[q] = state[j]; + state[j] = tmp; + + j = (j - (state[q] & 0xff)) & 0xff; + q = (q - 1) & 0xff; + + return true; + } + + public boolean undoRc4(int length) { + for (int i = 0; i < length; i++) { + if (!moveDown()) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/G-Earth/src/main/java/gearth/protocol/crypto/RC4Cipher.java b/G-Earth/src/main/java/gearth/protocol/crypto/RC4Cipher.java new file mode 100644 index 0000000..ecada39 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/crypto/RC4Cipher.java @@ -0,0 +1,21 @@ +package gearth.protocol.crypto; + +public interface RC4Cipher { + + byte[] cipher(byte[] data); + + byte[] cipher(byte[] data, int offset, int length); + + byte[] decipher(byte[] data); + + byte[] decipher(byte[] data, int offset, int length); + + byte[] getState(); + + int getQ(); + + int getJ(); + + RC4Cipher deepCopy(); + +} diff --git a/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java b/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java index 8911632..b741dc3 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java @@ -4,11 +4,16 @@ import gearth.GEarth; import gearth.protocol.HConnection; import gearth.protocol.HMessage; import gearth.protocol.crypto.RC4; +import gearth.protocol.crypto.RC4Base64; import gearth.protocol.memory.habboclient.HabboClient; import gearth.protocol.memory.habboclient.HabboClientFactory; import gearth.protocol.packethandler.EncryptedPacketHandler; -import gearth.protocol.packethandler.PayloadBuffer; +import gearth.protocol.packethandler.flash.FlashBuffer; import gearth.protocol.packethandler.flash.BufferChangeListener; +import gearth.protocol.packethandler.flash.FlashPacketHandler; +import gearth.protocol.packethandler.shockwave.ShockwavePacketOutgoingHandler; +import gearth.protocol.packethandler.PayloadBuffer; +import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer; import gearth.ui.titlebar.TitleBarController; import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; @@ -18,24 +23,51 @@ import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.Region; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class Rc4Obtainer { - public static final boolean DEBUG = false; + private static final Logger logger = LoggerFactory.getLogger(Rc4Obtainer.class); - private final HabboClient client; - private List flashPacketHandlers; + private final HConnection hConnection; + private final List flashPacketHandlers; public Rc4Obtainer(HConnection hConnection) { - client = HabboClientFactory.get(hConnection); + this.hConnection = hConnection; + this.flashPacketHandlers = new ArrayList<>(); + } + + private static void showErrorDialog() { + Alert alert = new Alert(Alert.AlertType.WARNING, LanguageBundle.get("alert.somethingwentwrong.title"), ButtonType.OK); + + FlowPane fp = new FlowPane(); + Label lbl = new Label(LanguageBundle.get("alert.somethingwentwrong.content").replaceAll("\\\\n", System.lineSeparator())); + Hyperlink link = new Hyperlink("https://github.com/sirjonasxx/G-Earth/wiki/Troubleshooting"); + fp.getChildren().addAll(lbl, link); + link.setOnAction(event -> { + GEarth.main.getHostServices().showDocument(link.getText()); + event.consume(); + }); + + alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); + alert.getDialogPane().setContent(fp); + alert.setOnCloseRequest(event -> GEarth.main.getHostServices().showDocument(link.getText())); + try { + TitleBarController.create(alert).showAlert(); + } catch (IOException e) { + e.printStackTrace(); + } } public void setFlashPacketHandlers(EncryptedPacketHandler... flashPacketHandlers) { - this.flashPacketHandlers = Arrays.asList(flashPacketHandlers); + this.flashPacketHandlers.addAll(Arrays.asList(flashPacketHandlers)); + for (EncryptedPacketHandler handler : flashPacketHandlers) { BufferChangeListener bufferChangeListener = new BufferChangeListener() { @Override @@ -55,10 +87,18 @@ public class Rc4Obtainer { flashPacketHandlers.forEach(EncryptedPacketHandler::block); - new Thread(() -> { + logger.info("Caught encrypted packet, attempting to find decryption keys"); + + final HabboClient client = HabboClientFactory.get(hConnection); + if (client == null) { + logger.info("Unsupported platform / client combination, aborting connection"); + hConnection.abort(); + return; + } + + new Thread(() -> { + final long startTime = System.currentTimeMillis(); - long startTime = System.currentTimeMillis(); - if (DEBUG) System.out.println("[+] send encrypted"); boolean worked = false; int i = 0; @@ -70,49 +110,93 @@ public class Rc4Obtainer { } if (!worked) { - System.err.println("COULD NOT FIND RC4 TABLE"); + try { + Platform.runLater(Rc4Obtainer::showErrorDialog); + } catch (IllegalStateException e) { + // ignore, thrown in tests. + } - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.WARNING, LanguageBundle.get("alert.somethingwentwrong.title"), ButtonType.OK); - - FlowPane fp = new FlowPane(); - Label lbl = new Label(LanguageBundle.get("alert.somethingwentwrong.content").replaceAll("\\\\n", System.lineSeparator())); - Hyperlink link = new Hyperlink("https://github.com/sirjonasxx/G-Earth/wiki/Troubleshooting"); - fp.getChildren().addAll(lbl, link); - link.setOnAction(event -> { - GEarth.main.getHostServices().showDocument(link.getText()); - event.consume(); - }); - - alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); - alert.getDialogPane().setContent(fp); - alert.setOnCloseRequest(event -> GEarth.main.getHostServices().showDocument(link.getText())); - try { - TitleBarController.create(alert).showAlert(); - } catch (IOException e) { - e.printStackTrace(); - } - }); + logger.error("Failed to find RC4 table, aborting connection"); + hConnection.abort(); + return; } final long endTime = System.currentTimeMillis(); - if (DEBUG) - System.out.println("Cracked RC4 in " + (endTime - startTime) + "ms"); + logger.info("Cracked decryption keys in {}ms", endTime - startTime); flashPacketHandlers.forEach(EncryptedPacketHandler::unblock); }).start(); } private boolean onSendFirstEncryptedMessage(EncryptedPacketHandler flashPacketHandler, List potentialRC4tables) { + if (potentialRC4tables == null || potentialRC4tables.isEmpty()) { + return false; + } - for (byte[] possible : potentialRC4tables) - if (isCorrectRC4Table(flashPacketHandler, possible)) + for (byte[] possible : potentialRC4tables) { + if (flashPacketHandler instanceof FlashPacketHandler && bruteFlash(flashPacketHandler, possible)) return true; + if (flashPacketHandler instanceof ShockwavePacketOutgoingHandler && bruteShockwaveHeader(flashPacketHandler, possible)) { + return true; + } + } + return false; } - private boolean isCorrectRC4Table(EncryptedPacketHandler flashPacketHandler, byte[] possible) { + private boolean bruteShockwaveHeader(EncryptedPacketHandler packetHandler, byte[] tableState) { + final int encBufferSize = packetHandler.getEncryptedBuffer().size(); + + if (encBufferSize < ShockwaveOutBuffer.PACKET_SIZE_MIN_ENCRYPTED) { + return false; + } + + // Copy buffer. + final byte[] encBuffer = new byte[encBufferSize]; + for (int i = 0; i < encBufferSize; i++) { + encBuffer[i] = packetHandler.getEncryptedBuffer().get(i); + } + + // Brute force q and j. + for (int q = 0; q < 256; q++) { + for (int j = 0; j < 256; j++) { + final byte[] tableStateCopy = Arrays.copyOf(tableState, tableState.length); + final RC4Base64 rc4 = new RC4Base64(tableStateCopy, q, j); + + if (packetHandler.getDirection() == HMessage.Direction.TOSERVER) { + // Encoded 3 headers, 4 * 3 = 12 + if (!rc4.undoRc4(12)) { + continue; + } + } + + final byte[] encDataCopy = Arrays.copyOf(encBuffer, encBuffer.length); + final RC4Base64 rc4Test = rc4.deepCopy(); + + // Attempt to exhaust buffer. + final ShockwaveOutBuffer buffer = new ShockwaveOutBuffer(); + + buffer.setCipher(rc4Test); + buffer.push(encDataCopy); + + try { + final byte[][] packets = buffer.receive(); + + if (packets.length == 3 && buffer.isEmpty()) { + packetHandler.setRc4(rc4); + return true; + } + } catch (IllegalArgumentException e) { + // ignore + } + } + } + + return false; + } + + private boolean bruteFlash(EncryptedPacketHandler flashPacketHandler, byte[] possible) { try { @@ -136,12 +220,13 @@ public class Rc4Obtainer { final RC4 rc4TryCopy = rc4Tryout.deepCopy(); try { - final PayloadBuffer payloadBuffer = new PayloadBuffer(); - final byte[] decoded = rc4TryCopy.rc4(encDataCopy); + final PayloadBuffer payloadBuffer = new FlashBuffer(); + final byte[] decoded = rc4TryCopy.cipher(encDataCopy); - payloadBuffer.pushAndReceive(decoded); + payloadBuffer.push(decoded); + payloadBuffer.receive(); - if (payloadBuffer.peak().length == 0) { + if (payloadBuffer.isEmpty()) { flashPacketHandler.setRc4(rc4Tryout); return true; } diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java index 48cba93..3032a93 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java @@ -2,8 +2,10 @@ package gearth.protocol.memory.habboclient; import gearth.misc.OSValidator; import gearth.protocol.HConnection; +import gearth.protocol.connection.HClient; import gearth.protocol.memory.habboclient.linux.LinuxHabboClient; import gearth.protocol.memory.habboclient.macOs.MacOsHabboClient; +import gearth.protocol.memory.habboclient.shockwave.ShockwaveMemoryClient; import gearth.protocol.memory.habboclient.windows.WindowsHabboClient; /** @@ -11,16 +13,18 @@ import gearth.protocol.memory.habboclient.windows.WindowsHabboClient; */ public class HabboClientFactory { - public static HabboClient get(HConnection connection) { - if (OSValidator.isUnix()) return new LinuxHabboClient(connection); - if (OSValidator.isWindows()) return new WindowsHabboClient(connection); - if (OSValidator.isMac()) return new MacOsHabboClient(connection); + if (connection.getClientType() == HClient.SHOCKWAVE) { + return new ShockwaveMemoryClient(connection); + } else { + if (OSValidator.isWindows()) return new WindowsHabboClient(connection); + if (OSValidator.isUnix()) return new LinuxHabboClient(connection); + if (OSValidator.isMac()) return new MacOsHabboClient(connection); + } // todo use rust if beneficial return null; } - } diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/shockwave/ShockwaveMemoryClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/shockwave/ShockwaveMemoryClient.java new file mode 100644 index 0000000..03f8365 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/shockwave/ShockwaveMemoryClient.java @@ -0,0 +1,83 @@ +package gearth.protocol.memory.habboclient.shockwave; + +import gearth.encoding.HexEncoding; +import gearth.misc.OSValidator; +import gearth.protocol.HConnection; +import gearth.protocol.memory.habboclient.HabboClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +public class ShockwaveMemoryClient extends HabboClient { + + private static final Logger logger = LoggerFactory.getLogger(ShockwaveMemoryClient.class); + + public ShockwaveMemoryClient(HConnection connection) { + super(connection); + } + + @Override + public List getRC4cached() { + return Collections.emptyList(); + } + + @Override + public List getRC4possibilities() { + final List result = new ArrayList<>(); + + try { + final HashSet potentialTables = dumpTables(); + + for (String potentialTable : potentialTables) { + result.add(HexEncoding.toBytes(potentialTable)); + } + } catch (IOException | URISyntaxException e) { + logger.error("Failed to read RC4 possibilities from the Shockwave client", e); + } + + // Reverse the list so that the most likely keys are at the top. + Collections.reverse(result); + + return result; + } + + private HashSet dumpTables() throws IOException, URISyntaxException { + String filePath = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); + + if (OSValidator.isWindows()) { + filePath += "\\G-MemZ.exe"; + } else { + filePath += "/G-MemZ"; + } + + final ProcessBuilder pb = new ProcessBuilder(filePath); + final Process p = pb.start(); + + final HashSet possibleData = new HashSet<>(); + + try { + final BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + + String line; + + while ((line = reader.readLine()) != null) { + if (line.length() == 512) { + possibleData.add(line); + } + } + } finally { + p.destroy(); + } + + return possibleData; + } +} diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/EncryptedPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/EncryptedPacketHandler.java index 5495a3f..f19ba56 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/EncryptedPacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/EncryptedPacketHandler.java @@ -4,7 +4,7 @@ import gearth.misc.listenerpattern.Observable; import gearth.protocol.HConnection; import gearth.protocol.HMessage; import gearth.protocol.TrafficListener; -import gearth.protocol.crypto.RC4; +import gearth.protocol.crypto.RC4Cipher; import gearth.protocol.packethandler.flash.BufferChangeListener; import gearth.services.extension_handler.ExtensionHandler; import org.slf4j.Logger; @@ -27,8 +27,8 @@ public abstract class EncryptedPacketHandler extends PacketHandler { private volatile boolean isEncryptedStream; private volatile List tempEncryptedBuffer; - private RC4 encryptCipher; - private RC4 decryptCipher; + private RC4Cipher encryptCipher; + private RC4Cipher decryptCipher; protected EncryptedPacketHandler(ExtensionHandler extensionHandler, Observable[] trafficObservables, HMessage.Direction direction) { super(extensionHandler, trafficObservables); @@ -73,16 +73,16 @@ public abstract class EncryptedPacketHandler extends PacketHandler { tempEncryptedBuffer.add(buffer[i]); } } else { - writeBuffer(decryptCipher.rc4(buffer)); + writeBuffer(buffer); } } protected byte[] encrypt(byte[] buffer) { - return encryptCipher.rc4(buffer); + return encryptCipher.cipher(buffer); } protected byte[] decrypt(byte[] buffer) { - return decryptCipher.rc4(buffer); + return decryptCipher.decipher(buffer); } protected abstract void writeOut(byte[] buffer) throws IOException; @@ -105,7 +105,11 @@ public abstract class EncryptedPacketHandler extends PacketHandler { isTempBlocked = false; } - public void setRc4(RC4 rc4) { + public boolean isCiphersSet() { + return encryptCipher != null && decryptCipher != null; + } + + public void setRc4(RC4Cipher rc4) { this.decryptCipher = rc4.deepCopy(); this.encryptCipher = rc4.deepCopy(); diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/PayloadBuffer.java b/G-Earth/src/main/java/gearth/protocol/packethandler/PayloadBuffer.java index 36c7045..0cd6830 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/PayloadBuffer.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/PayloadBuffer.java @@ -1,42 +1,28 @@ package gearth.protocol.packethandler; -import gearth.protocol.HPacket; +import gearth.protocol.crypto.RC4Cipher; -import java.util.ArrayList; -import java.util.Arrays; +public abstract class PayloadBuffer { -public class PayloadBuffer { + protected byte[] buffer; - private byte[] buffer = new byte[0]; - - public HPacket[] pushAndReceive(byte[] tcpData){ - push(tcpData); - return receive(); - } - public void push(byte[] tcpData) { - buffer = buffer.length == 0 ? tcpData.clone() : ByteArrayUtils.combineByteArrays(buffer, tcpData); - } - public HPacket[] receive() { - if (buffer.length < 6) return new HPacket[0]; - HPacket total = new HPacket(buffer); - - ArrayList all = new ArrayList<>(); - while (total.getBytesLength() >= 4 && total.getBytesLength() - 4 >= total.length()){ - all.add(new HPacket(Arrays.copyOfRange(buffer, 0, total.length() + 4))); - buffer = Arrays.copyOfRange(buffer, total.length() + 4, buffer.length); - total = new HPacket(buffer); - } - return all.toArray(new HPacket[all.size()]); + public PayloadBuffer() { + this.buffer = new byte[0]; } + /** + * Make sure to call deepCopy on the cipher if you use it. + */ + public abstract void setCipher(RC4Cipher cipher); - public byte[] peak() { - return buffer; + public void push(byte[] data) { + buffer = buffer.length == 0 ? data.clone() : ByteArrayUtils.combineByteArrays(buffer, data); } - public byte[] forceClear() { - byte[] buff = buffer; - buffer = new byte[0]; - return buff; + + public abstract byte[][] receive(); + + public boolean isEmpty() { + return buffer.length == 0; } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashBuffer.java b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashBuffer.java new file mode 100644 index 0000000..e9540dd --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashBuffer.java @@ -0,0 +1,30 @@ +package gearth.protocol.packethandler.flash; + +import gearth.protocol.HPacket; +import gearth.protocol.crypto.RC4Cipher; +import gearth.protocol.packethandler.PayloadBuffer; + +import java.util.ArrayList; +import java.util.Arrays; + +public class FlashBuffer extends PayloadBuffer { + + @Override + public void setCipher(RC4Cipher cipher) { + // not needed + } + + public byte[][] receive() { + if (buffer.length < 6) return new byte[0][]; + HPacket total = new HPacket(buffer); + + final ArrayList all = new ArrayList<>(); + while (total.getBytesLength() >= 4 && total.getBytesLength() - 4 >= total.length()) { + all.add(Arrays.copyOfRange(buffer, 0, total.length() + 4)); + buffer = Arrays.copyOfRange(buffer, total.length() + 4, buffer.length); + total = new HPacket(buffer); + } + return all.toArray(new byte[0][]); + } + +} diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java index 4b00b1c..f5cb1f6 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java @@ -5,7 +5,6 @@ import gearth.protocol.HMessage; import gearth.protocol.HPacket; import gearth.protocol.TrafficListener; import gearth.protocol.packethandler.EncryptedPacketHandler; -import gearth.protocol.packethandler.PayloadBuffer; import gearth.services.extension_handler.ExtensionHandler; import java.io.IOException; @@ -14,13 +13,13 @@ import java.io.OutputStream; public abstract class FlashPacketHandler extends EncryptedPacketHandler { private final OutputStream out; - private final PayloadBuffer payloadBuffer; + private final FlashBuffer payloadBuffer; private volatile boolean isDataStream; FlashPacketHandler(HMessage.Direction direction, OutputStream outputStream, Observable[] trafficObservables, ExtensionHandler extensionHandler) { super(extensionHandler, trafficObservables, direction); this.out = outputStream; - this.payloadBuffer = new PayloadBuffer(); + this.payloadBuffer = new FlashBuffer(); this.isDataStream = false; } @@ -32,6 +31,7 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler { isDataStream = true; } + @Override public void act(byte[] buffer) throws IOException { if (!isDataStream) { synchronized (sendLock) { @@ -40,7 +40,11 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler { return; } - super.act(buffer); + if (isEncryptedStream() && isCiphersSet()) { + super.act(decrypt(buffer)); + } else { + super.act(buffer); + } if (!isBlocked()) { flush(); @@ -59,6 +63,7 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler { payloadBuffer.push(buffer); } + @Override public boolean sendToStream(byte[] buffer) { return sendToStream(buffer, isEncryptedStream()); } @@ -77,10 +82,9 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler { public void flush() throws IOException { synchronized (flushLock) { - HPacket[] hpackets = payloadBuffer.receive(); - - for (HPacket hpacket : hpackets){ - HMessage hMessage = new HMessage(hpacket, getDirection(), currentIndex); + for (final byte[] packet : payloadBuffer.receive()){ + HPacket hPacket = new HPacket(packet); + HMessage hMessage = new HMessage(hPacket, getDirection(), currentIndex); boolean isencrypted = isEncryptedStream(); if (isDataStream) { diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java index 3f845ad..2d78656 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java @@ -6,7 +6,7 @@ import gearth.protocol.HPacket; import gearth.protocol.TrafficListener; import gearth.protocol.connection.proxy.nitro.websocket.NitroSession; import gearth.protocol.packethandler.PacketHandler; -import gearth.protocol.packethandler.PayloadBuffer; +import gearth.protocol.packethandler.flash.FlashBuffer; import gearth.services.extension_handler.ExtensionHandler; import org.eclipse.jetty.websocket.api.Session; import org.slf4j.Logger; @@ -21,14 +21,14 @@ public class NitroPacketHandler extends PacketHandler { private final HMessage.Direction direction; private final NitroSession session; - private final PayloadBuffer payloadBuffer; + private final FlashBuffer payloadBuffer; private final Object payloadLock; public NitroPacketHandler(HMessage.Direction direction, NitroSession session, ExtensionHandler extensionHandler, Observable[] trafficObservables) { super(extensionHandler, trafficObservables); this.direction = direction; this.session = session; - this.payloadBuffer = new PayloadBuffer(); + this.payloadBuffer = new FlashBuffer(); this.payloadLock = new Object(); } @@ -61,8 +61,9 @@ public class NitroPacketHandler extends PacketHandler { payloadBuffer.push(buffer); synchronized (payloadLock) { - for (HPacket packet : payloadBuffer.receive()) { - HMessage hMessage = new HMessage(packet, direction, currentIndex); + for (final byte[] packet : payloadBuffer.receive()) { + HPacket hPacket = new HPacket(packet); + HMessage hMessage = new HMessage(hPacket, direction, currentIndex); awaitListeners(hMessage, hMessage1 -> sendToStream(hMessage1.getPacket().toBytes())); currentIndex++; } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java index 111e279..1bcac8d 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketHandler.java @@ -1,66 +1,45 @@ package gearth.protocol.packethandler.shockwave; -import gearth.encoding.HexEncoding; import gearth.misc.listenerpattern.Observable; import gearth.protocol.HMessage; import gearth.protocol.HPacket; +import gearth.protocol.HPacketFormat; import gearth.protocol.TrafficListener; -import gearth.protocol.packethandler.PacketHandler; -import gearth.protocol.packethandler.shockwave.buffers.ShockwaveBuffer; -import gearth.protocol.packethandler.shockwave.crypto.RC4Shockwave; +import gearth.protocol.crypto.RC4Cipher; +import gearth.protocol.packethandler.EncryptedPacketHandler; +import gearth.protocol.packethandler.PayloadBuffer; import gearth.services.extension_handler.ExtensionHandler; -import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.OutputStream; -public abstract class ShockwavePacketHandler extends PacketHandler { - - // The first 20 bytes of the artificialKey. - public static final byte[] ARTIFICIAL_KEY = Hex.decode("14d288cdb0bc08c274809a7802962af98b41dec8"); +public abstract class ShockwavePacketHandler extends EncryptedPacketHandler { protected static final Logger logger = LoggerFactory.getLogger(ShockwavePacketHandler.class); private final HMessage.Direction direction; - private final ShockwaveBuffer payloadBuffer; + private final HPacketFormat format; + private final PayloadBuffer payloadBuffer; private final Object flushLock; protected final OutputStream outputStream; - private boolean isEncrypted; - private final RC4Shockwave decryptCipher; - private final RC4Shockwave encryptCipher; - - ShockwavePacketHandler(HMessage.Direction direction, ShockwaveBuffer payloadBuffer, OutputStream outputStream, ExtensionHandler extensionHandler, Observable[] trafficObservables) { - super(extensionHandler, trafficObservables); + ShockwavePacketHandler(HMessage.Direction direction, PayloadBuffer payloadBuffer, OutputStream outputStream, ExtensionHandler extensionHandler, Observable[] trafficObservables) { + super(extensionHandler, trafficObservables, direction); this.direction = direction; + this.format = direction == HMessage.Direction.TOSERVER ? HPacketFormat.WEDGIE_OUTGOING : HPacketFormat.WEDGIE_INCOMING; this.payloadBuffer = payloadBuffer; this.outputStream = outputStream; this.flushLock = new Object(); - this.isEncrypted = false; - this.decryptCipher = new RC4Shockwave(0, ARTIFICIAL_KEY); - this.encryptCipher = new RC4Shockwave(0, ARTIFICIAL_KEY); - } - - protected void setEncrypted() { - isEncrypted = true; } @Override public boolean sendToStream(byte[] buffer) { - return sendToStream(buffer, isEncrypted); - } - - private boolean sendToStream(byte[] buffer, boolean isEncrypted) { synchronized (sendLock) { try { - if (!isEncrypted) { - outputStream.write(buffer); - } else { - outputStream.write(HexEncoding.toHex(encryptCipher.crypt(buffer), true)); - } + outputStream.write(buffer); return true; } catch (IOException e) { logger.error("Failed to send packet to stream", e); @@ -71,20 +50,40 @@ public abstract class ShockwavePacketHandler extends PacketHandler { @Override public void act(byte[] buffer) throws IOException { - if (!isEncrypted) { - payloadBuffer.push(buffer); - } else { - payloadBuffer.push(decryptCipher.crypt(Hex.decode(buffer))); - } + super.act(buffer); - flush(); + if (!isBlocked()) { + flush(); + } + } + + @Override + protected void writeOut(byte[] buffer) throws IOException { + synchronized (sendLock) { + outputStream.write(buffer); + } + } + + @Override + protected void writeBuffer(byte[] buffer) { + payloadBuffer.push(buffer); + } + + @Override + public void setRc4(RC4Cipher rc4) { + payloadBuffer.setCipher(rc4); + super.setRc4(rc4); } public void flush() throws IOException { synchronized (flushLock) { - final HPacket[] packets = payloadBuffer.receive(); + final byte[][] packets = payloadBuffer.receive(); + + for (final byte[] rawPacket : packets) { + final HPacket packet = isEncryptedStream() + ? format.createPacket(decrypt(rawPacket)) + : format.createPacket(rawPacket); - for (final HPacket packet : packets){ packet.setIdentifierDirection(direction); final HMessage message = new HMessage(packet, direction, currentIndex); diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java index bfef136..795eb3d 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketIncomingHandler.java @@ -34,7 +34,7 @@ public class ShockwavePacketIncomingHandler extends ShockwavePacketHandler { if (packet.headerId() == ID_SECRET_KEY) { logger.info("Received SECRET_KEY from server, enabling encryption / decryption."); trafficObservables[0].removeListener(this); - outgoingHandler.setEncrypted(); + outgoingHandler.setEncryptedStream(); } } }); diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java index 313e501..2ed26f8 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/ShockwavePacketOutgoingHandler.java @@ -4,22 +4,55 @@ import gearth.encoding.Base64Encoding; import gearth.misc.listenerpattern.Observable; import gearth.protocol.HMessage; import gearth.protocol.TrafficListener; +import gearth.protocol.crypto.RC4Cipher; import gearth.protocol.packethandler.ByteArrayUtils; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer; import gearth.services.extension_handler.ExtensionHandler; import java.io.OutputStream; +import java.util.concurrent.ThreadLocalRandom; public class ShockwavePacketOutgoingHandler extends ShockwavePacketHandler { + private RC4Cipher headerEncoder; + public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Observable[] trafficObservables) { super(HMessage.Direction.TOSERVER, new ShockwaveOutBuffer(), outputStream, extensionHandler, trafficObservables); } @Override public boolean sendToStream(byte[] packet) { - byte[] bufferLen = Base64Encoding.encode(packet.length, 3); - byte[] buffer = ByteArrayUtils.combineByteArrays(bufferLen, packet); + byte[] bufferLen; + + if (isEncryptedStream()) { + if (headerEncoder == null) { + throw new IllegalStateException("Expected header encoder to be set for an encrypted stream."); + } + + // Encrypt packet. + packet = encrypt(packet); + + // Encrypt header. + final byte[] newPacketLen = Base64Encoding.encode(packet.length, 3); + final byte[] header = new byte[4]; + + header[0] = (byte) ThreadLocalRandom.current().nextInt(0, 127); + header[1] = newPacketLen[0]; + header[2] = newPacketLen[1]; + header[3] = newPacketLen[2]; + + bufferLen = headerEncoder.cipher(header); + } else { + bufferLen = Base64Encoding.encode(packet.length, 3); + } + + final byte[] buffer = ByteArrayUtils.combineByteArrays(bufferLen, packet); return super.sendToStream(buffer); } + + @Override + public void setRc4(RC4Cipher rc4) { + this.headerEncoder = rc4.deepCopy(); + super.setRc4(rc4); + } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveBuffer.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveBuffer.java deleted file mode 100644 index 81f6622..0000000 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveBuffer.java +++ /dev/null @@ -1,11 +0,0 @@ -package gearth.protocol.packethandler.shockwave.buffers; - -import gearth.protocol.HPacket; - -public interface ShockwaveBuffer { - - void push(byte[] data); - - HPacket[] receive(); - -} diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveInBuffer.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveInBuffer.java index 041572d..f18049a 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveInBuffer.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveInBuffer.java @@ -1,44 +1,39 @@ package gearth.protocol.packethandler.shockwave.buffers; -import gearth.protocol.HPacket; -import gearth.protocol.packethandler.ByteArrayUtils; -import gearth.protocol.packethandler.shockwave.packets.ShockPacket; -import gearth.protocol.packethandler.shockwave.packets.ShockPacketIncoming; +import gearth.protocol.crypto.RC4Cipher; +import gearth.protocol.packethandler.PayloadBuffer; import java.util.ArrayList; import java.util.Arrays; -public class ShockwaveInBuffer implements ShockwaveBuffer { - - private byte[] buffer = new byte[0]; +public class ShockwaveInBuffer extends PayloadBuffer { @Override - public void push(byte[] data) { - buffer = buffer.length == 0 ? data.clone() : ByteArrayUtils.combineByteArrays(buffer, data); + public void setCipher(RC4Cipher cipher) { + // We don't need to decrypt incoming packet headers, for now. } @Override - public HPacket[] receive() { + public byte[][] receive() { if (buffer.length < 3) { - return new ShockPacket[0]; + return new byte[0][]; } // Incoming packets are delimited by chr(1). // We need to split the buffer by chr(1) and then parse each packet. - ArrayList packets = new ArrayList<>(); + final ArrayList packets = new ArrayList<>(); int curPos = 0; for (int i = 0; i < buffer.length; i++) { if (buffer[i] == 1) { - byte[] packetData = Arrays.copyOfRange(buffer, curPos, i); - packets.add(new ShockPacketIncoming(packetData)); + packets.add(Arrays.copyOfRange(buffer, curPos, i)); curPos = i + 1; } } buffer = Arrays.copyOfRange(buffer, curPos, buffer.length); - return packets.toArray(new ShockPacket[0]); + return packets.toArray(new byte[0][]); } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveOutBuffer.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveOutBuffer.java index 9bc5fd6..18df6df 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveOutBuffer.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/buffers/ShockwaveOutBuffer.java @@ -1,50 +1,65 @@ package gearth.protocol.packethandler.shockwave.buffers; import gearth.encoding.Base64Encoding; -import gearth.protocol.HPacket; -import gearth.protocol.packethandler.ByteArrayUtils; -import gearth.protocol.packethandler.shockwave.packets.ShockPacket; -import gearth.protocol.packethandler.shockwave.packets.ShockPacketOutgoing; +import gearth.protocol.crypto.RC4Cipher; +import gearth.protocol.packethandler.PayloadBuffer; import java.util.ArrayList; import java.util.Arrays; -public class ShockwaveOutBuffer implements ShockwaveBuffer { +public class ShockwaveOutBuffer extends PayloadBuffer { - private static final int PACKET_LENGTH_SIZE = 3; - private static final int PACKET_HEADER_SIZE = 2; + public static final int PACKET_HEADER_SIZE = 2; - private static final int PACKET_SIZE_MIN = PACKET_LENGTH_SIZE + PACKET_HEADER_SIZE; + public static final int PACKET_LENGTH_SIZE_ENCRYPTED = 6; + public static final int PACKET_LENGTH_SIZE = 3; - private byte[] buffer = new byte[0]; + public static final int PACKET_SIZE_MIN = PACKET_HEADER_SIZE + PACKET_LENGTH_SIZE; + public static final int PACKET_SIZE_MIN_ENCRYPTED = PACKET_HEADER_SIZE + PACKET_LENGTH_SIZE_ENCRYPTED; + + private RC4Cipher cipher; @Override - public void push(byte[] data) { - buffer = buffer.length == 0 ? data.clone() : ByteArrayUtils.combineByteArrays(buffer, data); + public void setCipher(RC4Cipher cipher) { + this.cipher = cipher.deepCopy(); } @Override - public HPacket[] receive() { - if (buffer.length < PACKET_SIZE_MIN) { - return new ShockPacket[0]; + public byte[][] receive() { + final int packetLengthSize = this.cipher != null ? PACKET_LENGTH_SIZE_ENCRYPTED : PACKET_LENGTH_SIZE; + final int minPacketSize = this.cipher != null ? PACKET_SIZE_MIN_ENCRYPTED : PACKET_SIZE_MIN; + + if (buffer.length < minPacketSize) { + return new byte[0][]; } - ArrayList out = new ArrayList<>(); + final ArrayList out = new ArrayList<>(); - while (buffer.length >= PACKET_SIZE_MIN) { - int length = Base64Encoding.decode(new byte[]{buffer[0], buffer[1], buffer[2]}); - if (buffer.length < length + PACKET_LENGTH_SIZE) { + while (buffer.length >= minPacketSize) { + int length; + + if (this.cipher != null) { + final byte[] decData = this.cipher.decipher(buffer, 0, PACKET_LENGTH_SIZE_ENCRYPTED); + final int decDataLen = Base64Encoding.decode(new byte[]{decData[1], decData[2], decData[3]}); + + // TODO: Store length in a variable for if we don't have enough bytes. + + length = decDataLen; + } else { + length = Base64Encoding.decode(new byte[]{buffer[0], buffer[1], buffer[2]}); + } + + if (buffer.length < length + packetLengthSize) { break; } - int endPos = length + PACKET_LENGTH_SIZE; - byte[] packet = Arrays.copyOfRange(buffer, PACKET_LENGTH_SIZE, endPos); + int endPos = length + packetLengthSize; - out.add(new ShockPacketOutgoing(packet)); + out.add(Arrays.copyOfRange(buffer, packetLengthSize, endPos)); buffer = Arrays.copyOfRange(buffer, endPos, buffer.length); } - return out.toArray(new ShockPacket[0]); + return out.toArray(new byte[0][]); } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java b/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java deleted file mode 100644 index 102f491..0000000 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/shockwave/crypto/RC4Shockwave.java +++ /dev/null @@ -1,68 +0,0 @@ -package gearth.protocol.packethandler.shockwave.crypto; - -/** - * Habbo Shockwave RC4 is broken, meaning this is not a standard RC4 implementation. - * Thanks to Joni and DarkStar851 for discovering this. - */ -public class RC4Shockwave { - - private static final int TABLE_SIZE = 256; - - private final int[] table; - - private int x; - private int y; - - public RC4Shockwave(int key, byte[] artificialKey) { - this.table = buildTable(key, artificialKey); - } - - public byte[] crypt(byte[] data) { - byte[] result = new byte[data.length]; - - for (int i = 0; i < data.length; i++) { - x = (x + 1) % TABLE_SIZE; - y = (y + table[x] & 0xff) % TABLE_SIZE; - - swap(table, x, y); - - int xorIndex = ((table[x] & 0xff) + (table[y] & 0xff)) % TABLE_SIZE; - - result[i] = (byte) (data[i] ^ table[xorIndex & 0xff]); - } - - return result; - } - - private static int[] buildTable(int key, byte[] artificialKey) { - byte[] modKey = new byte[20]; - - for (int i = 0, j = 0; i < modKey.length; i++, j++) { - if (j >= artificialKey.length) { - j = 0; - } - - modKey[i] = (byte) (key & modKey[j]); - } - - int[] table = new int[TABLE_SIZE]; - - for (int i = 0; i < TABLE_SIZE; i++) { - table[i] = (byte) i; - } - - for (int q = 0, j = 0; q < TABLE_SIZE; q++) { - j = (j + (table[q] & 0xff) + modKey[q % modKey.length]) % TABLE_SIZE; - - swap(table, q, j); - } - - return table; - } - - private static void swap(int[] table, int i, int j) { - int temp = table[i]; - table[i] = table[j]; - table[j] = temp; - } -} diff --git a/G-Earth/src/main/resources/build/mac/G-MemZ b/G-Earth/src/main/resources/build/mac/G-MemZ new file mode 100644 index 0000000..8f529c6 Binary files /dev/null and b/G-Earth/src/main/resources/build/mac/G-MemZ differ diff --git a/G-Earth/src/main/resources/build/windows/32bit/G-MemZ.exe b/G-Earth/src/main/resources/build/windows/32bit/G-MemZ.exe new file mode 100644 index 0000000..b1872cc Binary files /dev/null and b/G-Earth/src/main/resources/build/windows/32bit/G-MemZ.exe differ diff --git a/G-Earth/src/main/resources/build/windows/64bit/G-MemZ.exe b/G-Earth/src/main/resources/build/windows/64bit/G-MemZ.exe new file mode 100644 index 0000000..b80e7e8 Binary files /dev/null and b/G-Earth/src/main/resources/build/windows/64bit/G-MemZ.exe differ diff --git a/G-Earth/src/test/java/TestRc4Shockwave.java b/G-Earth/src/test/java/TestRc4Shockwave.java new file mode 100644 index 0000000..cb8c367 --- /dev/null +++ b/G-Earth/src/test/java/TestRc4Shockwave.java @@ -0,0 +1,212 @@ +import gearth.protocol.HConnection; +import gearth.protocol.connection.HClient; +import gearth.protocol.crypto.RC4Base64; +import gearth.protocol.crypto.RC4Cipher; +import gearth.protocol.memory.Rc4Obtainer; +import gearth.protocol.memory.habboclient.HabboClientFactory; +import gearth.protocol.memory.habboclient.shockwave.ShockwaveMemoryClient; +import gearth.protocol.packethandler.EncryptedPacketHandler; +import gearth.protocol.packethandler.shockwave.ShockwavePacketOutgoingHandler; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mockStatic; + +public class TestRc4Shockwave { + + private final byte[] encryptedBuffer = Hex.decode("724148504d776b51545841624c6776583352355750713537696931482f39684e2f566466507841756a484f62684e6b4d666c44636e55676d30396c625573304b71754a303966786a5555503342536c32336b633239715577373956512f2b43384577657a5a4d65756b6258347436775061496a6b5539517a324b654a53326e4e4e4c3351666334"); + + private final byte[][] potentialTables = new byte[][] { + Hex.decode("cfa4debb4f28d4279d0f668aabba13810b8f7f4c7917eb1618296937c852d05a5d5f41ffc65c4d63bdacf22ebea27e913f56016fd6dae11ab749e068d59a99656ca8c91bf5779b4b03eda560fdc2742a2421517afa6242470e87f8c45edfbc59e650826e4a4810b9ca7df31e1db16175723354c564a67820e3b4dd5b3dd7b39f73a30d96053c71c795ae5707b611a04ead268eb5220cf9e7b2ee23f6bf9e76a7e5a1e9587c408583af3a8b7be8d13000fc86452f929738350206b0c398fb8dfe09f7c1e41ff18470d86d672d1c8ccc806bd332f025ef90a9ea08d26a89dbb8cbec4615f4e29c5544aa53c0d9883b433493ce0adc12cd2c1914942b36043e3139"), + Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6236c41f299c8a043960d986e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d9ae5750a0ee31139b2e4646f0f69c7a2ba0cf4dd7b3e57c69ca5dff11cb3da47d0310b72e995093420b87c5ceafbb9fa1055bdcbeec4c9bc2fc01bca67a11daa447308d66053d546949fd42e382d97f9f3be21ad71b4d1ef2ccf68807f78cc86150092dba7e050a89e904e17a9367e7754278c30b0a6d2f8"), + Hex.decode("b5c5af8acedbabb84f16481afa427a795967d1654b05c237382609191013f684e995bf8ecd2b4aad9da3a585fd4935ffd3bb249ba4b72572ec1e5b28886014b623d6ddf299787c43960d7b6e4dee80731fa87d520704fef08194635e4caeebcb708b5a47d9d5cf8301570644665f74338691daf5c1ed5840e2fc21de8761c303933d2a76d700f78d3ed0750ae0e3113953e4646f3169c02eaad24e46989aefb36ccc5cb9d4ba6b55e51dcaaca7fbc9f4e7081ca10e3a20188fd80fe81229b197020b2cdfc4f350c6716a82b46d7f6841bcc7f91b32c8bea09fdc3f51e19cf13ca2455de6b22d923b2215622f56eabd899e903417a9367e7754278c30b0a60cf8"), + Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6236c41f299c8a043960d986e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d9ae5750a0ee31139b2e4646f0f69c7a2ba0cf4dd7b3e57c69ca5dff11cb3da47d0310b72e995093420b87c5ceafbb9fa1055bdcbeec4c9bc2fc01bca67a11daa447308d66053d546949fd42e382d97f9f3be21ad71b4d1ef2ccf68807f78cc86150092dba7e050a89e904e17a9367e7754278c30b0a6d2f8"), + Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6239c41f299c8a043960d7b6e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d3ee5750a0ee31139b2e4646f0f69c7a2bad24e46989aefb36ccca1b9d4c6da47d0310b72e995093420b87c5ceafbf1fa1055bdcbeec4c9bc2fc01bca67df1daa447308d66053d5dd949f1c2e382d97f9f3be21ad71b4d1572ccf68807f78a586150092dba7e050a89e90f417a9367e7754278c30b0a60cf8"), + }; + + private final Semaphore waitSemaphore = new Semaphore(0); + private final AtomicReference cipher = new AtomicReference<>(); + + private final ShockwaveMemoryClient mockShockwaveMemoryClient = new ShockwaveMemoryClient(null) { + @Override + public List getRC4cached() { + return new ArrayList<>(); + } + + @Override + public List getRC4possibilities() { + return Arrays.asList(potentialTables); + } + }; + + private final HConnection mockConnection = new HConnection() { + @Override + public HClient getClientType() { + return HClient.SHOCKWAVE; + } + + @Override + public void abort() { + waitSemaphore.release(); + } + }; + + private final EncryptedPacketHandler mockEncryptedPacketHandler = new ShockwavePacketOutgoingHandler(null, null, null) { + @Override + public boolean isEncryptedStream() { + return true; + } + + @Override + public boolean sendToStream(byte[] buffer) { + return false; + } + + @Override + protected void writeOut(byte[] buffer) { } + + @Override + public void setRc4(RC4Cipher rc4) { + cipher.set(rc4); + waitSemaphore.release(); + } + }; + + @Test + public void testMoveUpDown() { + final RC4Base64 rc = new RC4Base64(potentialTables[0], 0, 0); + + final byte[] tableA = rc.getState().clone(); + final int tableA_X = rc.getQ(); + final int tableA_Y = rc.getJ(); + + rc.moveUp(); + rc.moveDown(); + + final byte[] tableB = rc.getState().clone(); + final int tableB_X = rc.getQ(); + final int tableB_Y = rc.getJ(); + + assertArrayEquals(tableA, tableB); + assertEquals(tableA_X, tableB_X); + assertEquals(tableA_Y, tableB_Y); + } + + @Test + public void testRc4Base64() { + final RC4Base64 c = new RC4Base64( + Hex.decode("D6EAA2D902B1797E759D5F8C26175B93BEC1235764E6F26972A6D85343B259CA715CB9418A19CC984EDB617F3E9E0947EB5A7D46ECAEC26E1C5D62E33D226D39337B0BD4783F49AC6A1FB8AB0A14CD7CC6F3D701895EE4E8D3F9FF8BF628E70058A183BD1B32813B31060F1DDC9B35E58D7740A320EE731584D5B30536A01116DF4854FA37742E2C50B6AF4B9A4D0D4F6BBF9CC3666CCE45D2A525FB4A8E182F3C2776A7F499C438210EA87AB5043463136512958F4CFC68C9B7C7C042BA109786A43A08DA2B1E55B4D0DDE9871A6FCF30F0E185FE5192800CDE29BCADEF2D03C5CBE2E0A9EDD12A5652FDF896F1F79491F5679F24600790BBC84482B070AA88"), + 152, + 211 + ); + + final byte[] out = c.decipher(Hex.decode("3270635A4F67")); + + assertEquals("01020304", Hex.toHexString(out)); + + final RC4Base64 c2 = new RC4Base64( + Hex.decode("D6EAA2D902B1797E759D5F8C26175B93BEC1235764E6F26972A6D85343B259CA715CB9418A19CC984EDB617F3E9E0947EB5A7D46ECAEC26E1C5D62E33D226D39337B0BD4783F49AC6A1FB8AB0A14CD7CC6F3D701895EE4E8D3F9FF8BF628E70058A183BD1B32813B31060F1DDC9B35E58D7740A320EE731584D5B30536A01116DF4854FA37742E2C50B6AF4B9A4D0D4F6BBF9CC3666CCE45D2A525FB4A8E182F3C2776A7F499C438210EA87AB5043463136512958F4CFC68C9B7C7C042BA109786A43A08DA2B1E55B4D0DDE9871A6FCF30F0E185FE5192800CDE29BCADEF2D03C5CBE2E0A9EDD12A5652FDF896F1F79491F5679F24600790BBC84482B070AA88"), + 152, + 211 + ); + + final byte[] out2 = c2.decipher(Hex.decode("3270635A4F714A4D742F43545551")); + + assertEquals("0102030405060708090a", Hex.toHexString(out2)); + + // Test with undo. + final RC4Base64 c3 = new RC4Base64( + Hex.decode("F2FD7883352075B654143213705596EBE2D166331F49A8A9B750D7DDE580F77BFC3982AA7D28F5E92E1785005947194136275BE0254F91F8606EC09A05FA5161C87FFF5286CD9BFBC4A15DB06C694EEEB388E399AE72F01C5608ADA44C93373F9D6D34121558BA84C60D7E897A8DF4D8D96A3A8C31A6EA90CF7C4A57B8D6ED792AAF7607DC03733C5F6230CEDF6511F9F11B2C106394FEB2BCDB640CCB2DCADE9E2FCCDA040AE1C240BD2B6838290EE7B1AC81928A2224425CC1B5A3EF71B477AB9F1E9723A0F6443B3D5A4B95C3438F450BD3A57467D2069821BF09C5161AE8F36B9C8BD5A2A701876F5EC7BBE68E48B93E0FE4D4ECC9465302D01D264D18BE"), + 156, + 238 + ); + + c3.undoRc4(4); + + final byte[] out3 = c3.decipher(Hex.decode("4A422B2B4441")); + + assertEquals("01020304", Hex.toHexString(out3)); + } + + @Test + public void testRc4Obtainer() throws Exception { + final byte[] initialTable = Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6236c41f299c8a043960d986e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d9ae5750a0ee31139b2e4646f0f69c7a2ba0cf4dd7b3e57c69ca5dff11cb3da47d0310b72e995093420b87c5ceafbb9fa1055bdcbeec4c9bc2fc01bca67a11daa447308d66053d546949fd42e382d97f9f3be21ad71b4d1ef2ccf68807f78cc86150092dba7e050a89e904e17a9367e7754278c30b0a6d2f8"); + final int initialQ = 152; + final int initialJ = 242; + + // Mock HabboClientFactory to inject our mocked G-MemZ client. + final MockedStatic mock = mockStatic(HabboClientFactory.class); + + mock.when(() -> HabboClientFactory.get(mockConnection)).thenReturn(mockShockwaveMemoryClient); + + // Run the RC4 obtainer. + final Rc4Obtainer obtainer = new Rc4Obtainer(mockConnection); + + obtainer.setFlashPacketHandlers(mockEncryptedPacketHandler); + + mockEncryptedPacketHandler.act(encryptedBuffer); + + waitSemaphore.acquire(); + + final RC4Cipher c = cipher.get(); + + // Validate an exact match. + assertNotNull(c); + assertArrayEquals(initialTable, c.getState()); + assertEquals(initialQ, c.getQ()); + assertEquals(initialJ, c.getJ()); + + mock.close(); + } + + @Test + public void testRc4StateMutation() { + final byte[] startTable = Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6236c41f299c8a043960d986e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d9ae5750a0ee31139b2e4646f0f69c7a2ba0cf4dd7b3e57c69ca5dff11cb3da47d0310b72e995093420b87c5ceafbb9fa1055bdcbeec4c9bc2fc01bca67a11daa447308d66053d546949fd42e382d97f9f3be21ad71b4d1ef2ccf68807f78cc86150092dba7e050a89e904e17a9367e7754278c30b0a6d2f8"); + final int startQ = 152; + final int startJ = 242; + + final byte[] h1Table = Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6236c41f299c8a043960d7b6e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d9ae5750a0ee31139b2e4646f0f69c7a2bad24e46983e57c69ca5dff11cb3da47d0310b72e995093420b87c5ceafbb9fa1055bdcbeec4c9bc2fc01bca67a11daa447308d66053d5dd949fd42e382d97f9f3be21ad71b4d1ef2ccf68807f78cc86150092dba7e050a89e90f417a9367e7754278c30b0a60cf8"); + final int h1Q = 156; + final int h1J = 74; + + final byte[] h2Table = Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6239c41f299c8a043960d7b6e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d3ee5750a0ee31139b2e4646f0f69c7a2bad24e46989aefb36ca5dff11cc6da47d0310b72e995093420b87c5ceafbb9fa1055bdcbeec4c9bc2fc01bca67a11daa447308d66053d5dd949fd42e382d97f9f3be21ad71b4d1572ccf68807f78cc86150092dba7e050a89e90f417a9367e7754278c30b0a60cf8"); + final int h2Q = 160; + final int h2J = 65; + + final byte[] h3Table = Hex.decode("b5c5af8ace02ab824f16481a18427a795929e7654b05c2373226b1198f13f684563abf8ecd2b4a519da36285fd4935ffd3bb249ba4b725acec1e5b28886d14b6239c41f299c8a043960d7b6e4d12e66a1f898b520704fef081dc635e4caeebe8707d5ad8d95d4583013c06e1665f74333b916bf5c1ed5840e2fc3fde8761c303933d2a76d722f78d3ee5750a0ee31139b2e4646f0f69c7a2bad24e46989aefb36ccca1b9d4c6da47d0310b72e995093420b87c5ceafbf1fa1055bdcbeec4c9bc2fc01bca67df1daa447308d66053d5dd949f1c2e382d97f9f3be21ad71b4d1572ccf68807f78a586150092dba7e050a89e90f417a9367e7754278c30b0a60cf8"); + final int h3Q = 164; + final int h3J = 210; + + // Create cipher. + final RC4Base64 cipher = new RC4Base64(startTable, startQ, startJ); + + // First header. + cipher.cipher(new byte[4]); + + assertArrayEquals(h1Table, cipher.getState()); + assertEquals(h1Q, cipher.getQ()); + assertEquals(h1J, cipher.getJ()); + + // Second header. + cipher.cipher(new byte[4]); + + assertArrayEquals(h2Table, cipher.getState()); + assertEquals(h2Q, cipher.getQ()); + assertEquals(h2J, cipher.getJ()); + + // Third header. + cipher.cipher(new byte[4]); + + assertArrayEquals(h3Table, cipher.getState()); + assertEquals(h3Q, cipher.getQ()); + assertEquals(h3J, cipher.getJ()); + } +}