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());
+ }
+}