Update to Habbo Origins v24

This commit is contained in:
UnfamiliarLegacy 2024-06-27 01:00:28 +02:00
parent c908bcbc44
commit d0c0e05725
24 changed files with 903 additions and 314 deletions

View File

@ -345,6 +345,12 @@
<version>5.10.2</version> <version>5.10.2</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<repositories> <repositories>

View File

@ -9,6 +9,7 @@ import gearth.protocol.connection.HStateSetter;
import gearth.protocol.connection.proxy.ProxyProvider; import gearth.protocol.connection.proxy.ProxyProvider;
import gearth.protocol.interceptor.ConnectionInterceptor; import gearth.protocol.interceptor.ConnectionInterceptor;
import gearth.protocol.interceptor.ConnectionInterceptorCallbacks; import gearth.protocol.interceptor.ConnectionInterceptorCallbacks;
import gearth.protocol.memory.Rc4Obtainer;
import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.PacketHandler;
import gearth.protocol.packethandler.shockwave.ShockwavePacketIncomingHandler; import gearth.protocol.packethandler.shockwave.ShockwavePacketIncomingHandler;
import gearth.protocol.packethandler.shockwave.ShockwavePacketOutgoingHandler; 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 ShockwavePacketOutgoingHandler outgoingHandler = new ShockwavePacketOutgoingHandler(server.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables());
final ShockwavePacketIncomingHandler incomingHandler = new ShockwavePacketIncomingHandler(client.getOutputStream(), hConnection.getExtensionHandler(), hConnection.getTrafficObservables(), outgoingHandler); 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. // Lets revisit when origins is more mature.
proxy.verifyProxy(incomingHandler, outgoingHandler, "20", "SHOCKWAVE"); proxy.verifyProxy(incomingHandler, outgoingHandler, "24", "SHOCKWAVE");
hProxySetter.setProxy(proxy); hProxySetter.setProxy(proxy);
onConnect(); onConnect();
@ -107,7 +111,6 @@ public class ShockwaveProxy implements ProxyProvider, ConnectionInterceptorCallb
try { try {
if (!server.isClosed()) server.close(); if (!server.isClosed()) server.close();
if (!client.isClosed()) client.close(); if (!client.isClosed()) client.close();
if (HConnection.DEBUG) System.out.println("STOP");
onConnectEnd(); onConnectEnd();
} catch (IOException e) { } catch (IOException e) {
logger.error("Error occurred while closing sockets.", e); logger.error("Error occurred while closing sockets.", e);

View File

@ -54,9 +54,9 @@ import java.util.Arrays;
* <p> * <p>
* @author Clarence Ho * @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 x;
private int y; private int y;
@ -79,9 +79,8 @@ public class RC4 {
* @param key the encryption/decryption key * @param key the encryption/decryption key
*/ */
public RC4(byte[] key) throws NullPointerException { public RC4(byte[] key) throws NullPointerException {
for (int i = 0; i < 256; i++) {
for (int i=0; i < 256; i++) { state[i] = (byte) i;
state[i] = (byte)i;
} }
x = 0; x = 0;
@ -96,8 +95,7 @@ public class RC4 {
throw new NullPointerException(); 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; index2 = ((key[index1] & 0xff) + (state[i] & 0xff) + index2) & 0xff;
tmp = state[i]; tmp = state[i];
@ -106,9 +104,6 @@ public class RC4 {
index1 = (index1 + 1) % key.length; index1 = (index1 + 1) % key.length;
} }
} }
public RC4(byte[] state, int x, int y) { public RC4(byte[] state, int x, int y) {
@ -117,51 +112,29 @@ public class RC4 {
this.state = state; this.state = state;
} }
//copyconstructor
public RC4 deepCopy() {
return new RC4(Arrays.copyOf(state, 256), x, y);
}
/** /**
* RC4 encryption/decryption. * 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 * @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) { if (data == null) {
return null; return null;
} }
byte[] tmp = data.getBytes(); byte[] result = new byte[length];
this.rc4(tmp); for (int i = 0; i < length; i++) {
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++) {
x = (x + 1) & 0xff; x = (x + 1) & 0xff;
y = ((state[x] & 0xff) + y) & 0xff; y = ((state[x] & 0xff) + y) & 0xff;
@ -170,26 +143,50 @@ public class RC4 {
state[x] = state[y]; state[x] = state[y];
state[y] = tmp; state[y] = tmp;
xorIndex = ((state[x] &0xff) + (state[y] & 0xff)) & 0xff; xorIndex = ((state[x] & 0xff) + (state[y] & 0xff)) & 0xff;
result[i] = (byte)(buf[i] ^ state[xorIndex]); result[i] = (byte) (data[offset + i] ^ state[xorIndex]);
} }
//this.x = lx;
//this.y = ly;
return result; 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() { public boolean couldBeFresh() {
return (x == 0 && y == 0); return (x == 0 && y == 0);
} }
public void undoRc4(byte[] buf) { public void undoRc4(byte[] buf) {
byte tmp; byte tmp;
for (int i = buf.length - 1; i >= 0; i--) { for (int i = 0; i < buf.length; i++) {
tmp = state[x]; tmp = state[x];
state[x] = state[y]; state[x] = state[y];
state[y] = tmp; state[y] = tmp;
@ -197,10 +194,5 @@ public class RC4 {
y = (y - (state[x] & 0xff)) & 0xff; y = (y - (state[x] & 0xff)) & 0xff;
x = (x - 1) & 0xff; x = (x - 1) & 0xff;
} }
}
public byte[] getState () {
return state;
} }
} }

View File

@ -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.
* <a href="https://github.com/aromaa/Skylight3/blob/72ec3a07d126de09f6de4251c91001329f77a8a2/src/Skylight.Server/Net/Crypto/RC4Base64.cs">
* https://github.com/aromaa/Skylight3/blob/72ec3a07d126de09f6de4251c91001329f77a8a2/src/Skylight.Server/Net/Crypto/RC4Base64.cs
* </a>
*/
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;
}
}

View File

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

View File

@ -4,11 +4,16 @@ import gearth.GEarth;
import gearth.protocol.HConnection; import gearth.protocol.HConnection;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.crypto.RC4; import gearth.protocol.crypto.RC4;
import gearth.protocol.crypto.RC4Base64;
import gearth.protocol.memory.habboclient.HabboClient; import gearth.protocol.memory.habboclient.HabboClient;
import gearth.protocol.memory.habboclient.HabboClientFactory; import gearth.protocol.memory.habboclient.HabboClientFactory;
import gearth.protocol.packethandler.EncryptedPacketHandler; 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.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.titlebar.TitleBarController;
import gearth.ui.translations.LanguageBundle; import gearth.ui.translations.LanguageBundle;
import javafx.application.Platform; import javafx.application.Platform;
@ -18,61 +23,27 @@ import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public class Rc4Obtainer { public class Rc4Obtainer {
public static final boolean DEBUG = false; private static final Logger logger = LoggerFactory.getLogger(Rc4Obtainer.class);
private final HabboClient client; private final HConnection hConnection;
private List<EncryptedPacketHandler> flashPacketHandlers; private final List<EncryptedPacketHandler> flashPacketHandlers;
public Rc4Obtainer(HConnection hConnection) { public Rc4Obtainer(HConnection hConnection) {
client = HabboClientFactory.get(hConnection); this.hConnection = hConnection;
this.flashPacketHandlers = new ArrayList<>();
} }
public void setFlashPacketHandlers(EncryptedPacketHandler... flashPacketHandlers) { private static void showErrorDialog() {
this.flashPacketHandlers = Arrays.asList(flashPacketHandlers);
for (EncryptedPacketHandler handler : flashPacketHandlers) {
BufferChangeListener bufferChangeListener = new BufferChangeListener() {
@Override
public void onPacket() {
if (handler.isEncryptedStream()) {
onSendFirstEncryptedMessage(handler);
handler.getPacketReceivedObservable().removeListener(this);
}
}
};
handler.getPacketReceivedObservable().addListener(bufferChangeListener);
}
}
private void onSendFirstEncryptedMessage(EncryptedPacketHandler flashPacketHandler) {
if (!HConnection.DECRYPTPACKETS) return;
flashPacketHandlers.forEach(EncryptedPacketHandler::block);
new Thread(() -> {
long startTime = System.currentTimeMillis();
if (DEBUG) System.out.println("[+] send encrypted");
boolean worked = false;
int i = 0;
while (!worked && i < 4) {
worked = (i % 2 == 0) ?
onSendFirstEncryptedMessage(flashPacketHandler, client.getRC4cached()) :
onSendFirstEncryptedMessage(flashPacketHandler, client.getRC4possibilities());
i++;
}
if (!worked) {
System.err.println("COULD NOT FIND RC4 TABLE");
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.WARNING, LanguageBundle.get("alert.somethingwentwrong.title"), ButtonType.OK); Alert alert = new Alert(Alert.AlertType.WARNING, LanguageBundle.get("alert.somethingwentwrong.title"), ButtonType.OK);
FlowPane fp = new FlowPane(); FlowPane fp = new FlowPane();
@ -92,27 +63,140 @@ public class Rc4Obtainer {
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
}); }
public void setFlashPacketHandlers(EncryptedPacketHandler... flashPacketHandlers) {
this.flashPacketHandlers.addAll(Arrays.asList(flashPacketHandlers));
for (EncryptedPacketHandler handler : flashPacketHandlers) {
BufferChangeListener bufferChangeListener = new BufferChangeListener() {
@Override
public void onPacket() {
if (handler.isEncryptedStream()) {
onSendFirstEncryptedMessage(handler);
handler.getPacketReceivedObservable().removeListener(this);
}
}
};
handler.getPacketReceivedObservable().addListener(bufferChangeListener);
}
}
private void onSendFirstEncryptedMessage(EncryptedPacketHandler flashPacketHandler) {
if (!HConnection.DECRYPTPACKETS) return;
flashPacketHandlers.forEach(EncryptedPacketHandler::block);
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();
boolean worked = false;
int i = 0;
while (!worked && i < 4) {
worked = (i % 2 == 0) ?
onSendFirstEncryptedMessage(flashPacketHandler, client.getRC4cached()) :
onSendFirstEncryptedMessage(flashPacketHandler, client.getRC4possibilities());
i++;
}
if (!worked) {
try {
Platform.runLater(Rc4Obtainer::showErrorDialog);
} catch (IllegalStateException e) {
// ignore, thrown in tests.
}
logger.error("Failed to find RC4 table, aborting connection");
hConnection.abort();
return;
} }
final long endTime = System.currentTimeMillis(); final long endTime = System.currentTimeMillis();
if (DEBUG) logger.info("Cracked decryption keys in {}ms", endTime - startTime);
System.out.println("Cracked RC4 in " + (endTime - startTime) + "ms");
flashPacketHandlers.forEach(EncryptedPacketHandler::unblock); flashPacketHandlers.forEach(EncryptedPacketHandler::unblock);
}).start(); }).start();
} }
private boolean onSendFirstEncryptedMessage(EncryptedPacketHandler flashPacketHandler, List<byte[]> potentialRC4tables) { private boolean onSendFirstEncryptedMessage(EncryptedPacketHandler flashPacketHandler, List<byte[]> potentialRC4tables) {
if (potentialRC4tables == null || potentialRC4tables.isEmpty()) {
return false;
}
for (byte[] possible : potentialRC4tables) for (byte[] possible : potentialRC4tables) {
if (isCorrectRC4Table(flashPacketHandler, possible)) if (flashPacketHandler instanceof FlashPacketHandler && bruteFlash(flashPacketHandler, possible))
return true; return true;
if (flashPacketHandler instanceof ShockwavePacketOutgoingHandler && bruteShockwaveHeader(flashPacketHandler, possible)) {
return true;
}
}
return false; 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 { try {
@ -136,12 +220,13 @@ public class Rc4Obtainer {
final RC4 rc4TryCopy = rc4Tryout.deepCopy(); final RC4 rc4TryCopy = rc4Tryout.deepCopy();
try { try {
final PayloadBuffer payloadBuffer = new PayloadBuffer(); final PayloadBuffer payloadBuffer = new FlashBuffer();
final byte[] decoded = rc4TryCopy.rc4(encDataCopy); 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); flashPacketHandler.setRc4(rc4Tryout);
return true; return true;
} }

View File

@ -2,8 +2,10 @@ package gearth.protocol.memory.habboclient;
import gearth.misc.OSValidator; import gearth.misc.OSValidator;
import gearth.protocol.HConnection; import gearth.protocol.HConnection;
import gearth.protocol.connection.HClient;
import gearth.protocol.memory.habboclient.linux.LinuxHabboClient; import gearth.protocol.memory.habboclient.linux.LinuxHabboClient;
import gearth.protocol.memory.habboclient.macOs.MacOsHabboClient; import gearth.protocol.memory.habboclient.macOs.MacOsHabboClient;
import gearth.protocol.memory.habboclient.shockwave.ShockwaveMemoryClient;
import gearth.protocol.memory.habboclient.windows.WindowsHabboClient; import gearth.protocol.memory.habboclient.windows.WindowsHabboClient;
/** /**
@ -11,16 +13,18 @@ import gearth.protocol.memory.habboclient.windows.WindowsHabboClient;
*/ */
public class HabboClientFactory { public class HabboClientFactory {
public static HabboClient get(HConnection connection) { public static HabboClient get(HConnection connection) {
if (OSValidator.isUnix()) return new LinuxHabboClient(connection); if (connection.getClientType() == HClient.SHOCKWAVE) {
return new ShockwaveMemoryClient(connection);
} else {
if (OSValidator.isWindows()) return new WindowsHabboClient(connection); if (OSValidator.isWindows()) return new WindowsHabboClient(connection);
if (OSValidator.isUnix()) return new LinuxHabboClient(connection);
if (OSValidator.isMac()) return new MacOsHabboClient(connection); if (OSValidator.isMac()) return new MacOsHabboClient(connection);
}
// todo use rust if beneficial // todo use rust if beneficial
return null; return null;
} }
} }

View File

@ -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<byte[]> getRC4cached() {
return Collections.emptyList();
}
@Override
public List<byte[]> getRC4possibilities() {
final List<byte[]> result = new ArrayList<>();
try {
final HashSet<String> 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<String> 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<String> 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;
}
}

View File

@ -4,7 +4,7 @@ import gearth.misc.listenerpattern.Observable;
import gearth.protocol.HConnection; import gearth.protocol.HConnection;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.TrafficListener; import gearth.protocol.TrafficListener;
import gearth.protocol.crypto.RC4; import gearth.protocol.crypto.RC4Cipher;
import gearth.protocol.packethandler.flash.BufferChangeListener; import gearth.protocol.packethandler.flash.BufferChangeListener;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -27,8 +27,8 @@ public abstract class EncryptedPacketHandler extends PacketHandler {
private volatile boolean isEncryptedStream; private volatile boolean isEncryptedStream;
private volatile List<Byte> tempEncryptedBuffer; private volatile List<Byte> tempEncryptedBuffer;
private RC4 encryptCipher; private RC4Cipher encryptCipher;
private RC4 decryptCipher; private RC4Cipher decryptCipher;
protected EncryptedPacketHandler(ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables, HMessage.Direction direction) { protected EncryptedPacketHandler(ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables, HMessage.Direction direction) {
super(extensionHandler, trafficObservables); super(extensionHandler, trafficObservables);
@ -73,16 +73,16 @@ public abstract class EncryptedPacketHandler extends PacketHandler {
tempEncryptedBuffer.add(buffer[i]); tempEncryptedBuffer.add(buffer[i]);
} }
} else { } else {
writeBuffer(decryptCipher.rc4(buffer)); writeBuffer(buffer);
} }
} }
protected byte[] encrypt(byte[] buffer) { protected byte[] encrypt(byte[] buffer) {
return encryptCipher.rc4(buffer); return encryptCipher.cipher(buffer);
} }
protected byte[] decrypt(byte[] buffer) { protected byte[] decrypt(byte[] buffer) {
return decryptCipher.rc4(buffer); return decryptCipher.decipher(buffer);
} }
protected abstract void writeOut(byte[] buffer) throws IOException; protected abstract void writeOut(byte[] buffer) throws IOException;
@ -105,7 +105,11 @@ public abstract class EncryptedPacketHandler extends PacketHandler {
isTempBlocked = false; 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.decryptCipher = rc4.deepCopy();
this.encryptCipher = rc4.deepCopy(); this.encryptCipher = rc4.deepCopy();

View File

@ -1,42 +1,28 @@
package gearth.protocol.packethandler; package gearth.protocol.packethandler;
import gearth.protocol.HPacket; import gearth.protocol.crypto.RC4Cipher;
import java.util.ArrayList; public abstract class PayloadBuffer {
import java.util.Arrays;
public class PayloadBuffer { protected byte[] buffer;
private byte[] buffer = new byte[0]; public PayloadBuffer() {
this.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<HPacket> 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()]);
} }
/**
* Make sure to call deepCopy on the cipher if you use it.
*/
public abstract void setCipher(RC4Cipher cipher);
public byte[] peak() { public void push(byte[] data) {
return buffer; buffer = buffer.length == 0 ? data.clone() : ByteArrayUtils.combineByteArrays(buffer, data);
} }
public byte[] forceClear() {
byte[] buff = buffer; public abstract byte[][] receive();
buffer = new byte[0];
return buff; public boolean isEmpty() {
return buffer.length == 0;
} }
} }

View File

@ -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<byte[]> 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][]);
}
}

View File

@ -5,7 +5,6 @@ import gearth.protocol.HMessage;
import gearth.protocol.HPacket; import gearth.protocol.HPacket;
import gearth.protocol.TrafficListener; import gearth.protocol.TrafficListener;
import gearth.protocol.packethandler.EncryptedPacketHandler; import gearth.protocol.packethandler.EncryptedPacketHandler;
import gearth.protocol.packethandler.PayloadBuffer;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import java.io.IOException; import java.io.IOException;
@ -14,13 +13,13 @@ import java.io.OutputStream;
public abstract class FlashPacketHandler extends EncryptedPacketHandler { public abstract class FlashPacketHandler extends EncryptedPacketHandler {
private final OutputStream out; private final OutputStream out;
private final PayloadBuffer payloadBuffer; private final FlashBuffer payloadBuffer;
private volatile boolean isDataStream; private volatile boolean isDataStream;
FlashPacketHandler(HMessage.Direction direction, OutputStream outputStream, Observable<TrafficListener>[] trafficObservables, ExtensionHandler extensionHandler) { FlashPacketHandler(HMessage.Direction direction, OutputStream outputStream, Observable<TrafficListener>[] trafficObservables, ExtensionHandler extensionHandler) {
super(extensionHandler, trafficObservables, direction); super(extensionHandler, trafficObservables, direction);
this.out = outputStream; this.out = outputStream;
this.payloadBuffer = new PayloadBuffer(); this.payloadBuffer = new FlashBuffer();
this.isDataStream = false; this.isDataStream = false;
} }
@ -32,6 +31,7 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler {
isDataStream = true; isDataStream = true;
} }
@Override
public void act(byte[] buffer) throws IOException { public void act(byte[] buffer) throws IOException {
if (!isDataStream) { if (!isDataStream) {
synchronized (sendLock) { synchronized (sendLock) {
@ -40,7 +40,11 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler {
return; return;
} }
if (isEncryptedStream() && isCiphersSet()) {
super.act(decrypt(buffer));
} else {
super.act(buffer); super.act(buffer);
}
if (!isBlocked()) { if (!isBlocked()) {
flush(); flush();
@ -59,6 +63,7 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler {
payloadBuffer.push(buffer); payloadBuffer.push(buffer);
} }
@Override
public boolean sendToStream(byte[] buffer) { public boolean sendToStream(byte[] buffer) {
return sendToStream(buffer, isEncryptedStream()); return sendToStream(buffer, isEncryptedStream());
} }
@ -77,10 +82,9 @@ public abstract class FlashPacketHandler extends EncryptedPacketHandler {
public void flush() throws IOException { public void flush() throws IOException {
synchronized (flushLock) { synchronized (flushLock) {
HPacket[] hpackets = payloadBuffer.receive(); for (final byte[] packet : payloadBuffer.receive()){
HPacket hPacket = new HPacket(packet);
for (HPacket hpacket : hpackets){ HMessage hMessage = new HMessage(hPacket, getDirection(), currentIndex);
HMessage hMessage = new HMessage(hpacket, getDirection(), currentIndex);
boolean isencrypted = isEncryptedStream(); boolean isencrypted = isEncryptedStream();
if (isDataStream) { if (isDataStream) {

View File

@ -6,7 +6,7 @@ import gearth.protocol.HPacket;
import gearth.protocol.TrafficListener; import gearth.protocol.TrafficListener;
import gearth.protocol.connection.proxy.nitro.websocket.NitroSession; import gearth.protocol.connection.proxy.nitro.websocket.NitroSession;
import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.PacketHandler;
import gearth.protocol.packethandler.PayloadBuffer; import gearth.protocol.packethandler.flash.FlashBuffer;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.Session;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -21,14 +21,14 @@ public class NitroPacketHandler extends PacketHandler {
private final HMessage.Direction direction; private final HMessage.Direction direction;
private final NitroSession session; private final NitroSession session;
private final PayloadBuffer payloadBuffer; private final FlashBuffer payloadBuffer;
private final Object payloadLock; private final Object payloadLock;
public NitroPacketHandler(HMessage.Direction direction, NitroSession session, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) { public NitroPacketHandler(HMessage.Direction direction, NitroSession session, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) {
super(extensionHandler, trafficObservables); super(extensionHandler, trafficObservables);
this.direction = direction; this.direction = direction;
this.session = session; this.session = session;
this.payloadBuffer = new PayloadBuffer(); this.payloadBuffer = new FlashBuffer();
this.payloadLock = new Object(); this.payloadLock = new Object();
} }
@ -61,8 +61,9 @@ public class NitroPacketHandler extends PacketHandler {
payloadBuffer.push(buffer); payloadBuffer.push(buffer);
synchronized (payloadLock) { synchronized (payloadLock) {
for (HPacket packet : payloadBuffer.receive()) { for (final byte[] packet : payloadBuffer.receive()) {
HMessage hMessage = new HMessage(packet, direction, currentIndex); HPacket hPacket = new HPacket(packet);
HMessage hMessage = new HMessage(hPacket, direction, currentIndex);
awaitListeners(hMessage, hMessage1 -> sendToStream(hMessage1.getPacket().toBytes())); awaitListeners(hMessage, hMessage1 -> sendToStream(hMessage1.getPacket().toBytes()));
currentIndex++; currentIndex++;
} }

View File

@ -1,66 +1,45 @@
package gearth.protocol.packethandler.shockwave; package gearth.protocol.packethandler.shockwave;
import gearth.encoding.HexEncoding;
import gearth.misc.listenerpattern.Observable; import gearth.misc.listenerpattern.Observable;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.HPacket; import gearth.protocol.HPacket;
import gearth.protocol.HPacketFormat;
import gearth.protocol.TrafficListener; import gearth.protocol.TrafficListener;
import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.crypto.RC4Cipher;
import gearth.protocol.packethandler.shockwave.buffers.ShockwaveBuffer; import gearth.protocol.packethandler.EncryptedPacketHandler;
import gearth.protocol.packethandler.shockwave.crypto.RC4Shockwave; import gearth.protocol.packethandler.PayloadBuffer;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
public abstract class ShockwavePacketHandler extends PacketHandler { public abstract class ShockwavePacketHandler extends EncryptedPacketHandler {
// The first 20 bytes of the artificialKey.
public static final byte[] ARTIFICIAL_KEY = Hex.decode("14d288cdb0bc08c274809a7802962af98b41dec8");
protected static final Logger logger = LoggerFactory.getLogger(ShockwavePacketHandler.class); protected static final Logger logger = LoggerFactory.getLogger(ShockwavePacketHandler.class);
private final HMessage.Direction direction; private final HMessage.Direction direction;
private final ShockwaveBuffer payloadBuffer; private final HPacketFormat format;
private final PayloadBuffer payloadBuffer;
private final Object flushLock; private final Object flushLock;
protected final OutputStream outputStream; protected final OutputStream outputStream;
private boolean isEncrypted; ShockwavePacketHandler(HMessage.Direction direction, PayloadBuffer payloadBuffer, OutputStream outputStream, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) {
private final RC4Shockwave decryptCipher; super(extensionHandler, trafficObservables, direction);
private final RC4Shockwave encryptCipher;
ShockwavePacketHandler(HMessage.Direction direction, ShockwaveBuffer payloadBuffer, OutputStream outputStream, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) {
super(extensionHandler, trafficObservables);
this.direction = direction; this.direction = direction;
this.format = direction == HMessage.Direction.TOSERVER ? HPacketFormat.WEDGIE_OUTGOING : HPacketFormat.WEDGIE_INCOMING;
this.payloadBuffer = payloadBuffer; this.payloadBuffer = payloadBuffer;
this.outputStream = outputStream; this.outputStream = outputStream;
this.flushLock = new Object(); 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 @Override
public boolean sendToStream(byte[] buffer) { public boolean sendToStream(byte[] buffer) {
return sendToStream(buffer, isEncrypted);
}
private boolean sendToStream(byte[] buffer, boolean isEncrypted) {
synchronized (sendLock) { synchronized (sendLock) {
try { try {
if (!isEncrypted) {
outputStream.write(buffer); outputStream.write(buffer);
} else {
outputStream.write(HexEncoding.toHex(encryptCipher.crypt(buffer), true));
}
return true; return true;
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to send packet to stream", e); logger.error("Failed to send packet to stream", e);
@ -71,20 +50,40 @@ public abstract class ShockwavePacketHandler extends PacketHandler {
@Override @Override
public void act(byte[] buffer) throws IOException { public void act(byte[] buffer) throws IOException {
if (!isEncrypted) { super.act(buffer);
payloadBuffer.push(buffer);
} else { if (!isBlocked()) {
payloadBuffer.push(decryptCipher.crypt(Hex.decode(buffer))); flush();
}
} }
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 { public void flush() throws IOException {
synchronized (flushLock) { 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); packet.setIdentifierDirection(direction);
final HMessage message = new HMessage(packet, direction, currentIndex); final HMessage message = new HMessage(packet, direction, currentIndex);

View File

@ -34,7 +34,7 @@ public class ShockwavePacketIncomingHandler extends ShockwavePacketHandler {
if (packet.headerId() == ID_SECRET_KEY) { if (packet.headerId() == ID_SECRET_KEY) {
logger.info("Received SECRET_KEY from server, enabling encryption / decryption."); logger.info("Received SECRET_KEY from server, enabling encryption / decryption.");
trafficObservables[0].removeListener(this); trafficObservables[0].removeListener(this);
outgoingHandler.setEncrypted(); outgoingHandler.setEncryptedStream();
} }
} }
}); });

View File

@ -4,22 +4,55 @@ import gearth.encoding.Base64Encoding;
import gearth.misc.listenerpattern.Observable; import gearth.misc.listenerpattern.Observable;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.TrafficListener; import gearth.protocol.TrafficListener;
import gearth.protocol.crypto.RC4Cipher;
import gearth.protocol.packethandler.ByteArrayUtils; import gearth.protocol.packethandler.ByteArrayUtils;
import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer; import gearth.protocol.packethandler.shockwave.buffers.ShockwaveOutBuffer;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.concurrent.ThreadLocalRandom;
public class ShockwavePacketOutgoingHandler extends ShockwavePacketHandler { public class ShockwavePacketOutgoingHandler extends ShockwavePacketHandler {
private RC4Cipher headerEncoder;
public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) { public ShockwavePacketOutgoingHandler(OutputStream outputStream, ExtensionHandler extensionHandler, Observable<TrafficListener>[] trafficObservables) {
super(HMessage.Direction.TOSERVER, new ShockwaveOutBuffer(), outputStream, extensionHandler, trafficObservables); super(HMessage.Direction.TOSERVER, new ShockwaveOutBuffer(), outputStream, extensionHandler, trafficObservables);
} }
@Override @Override
public boolean sendToStream(byte[] packet) { public boolean sendToStream(byte[] packet) {
byte[] bufferLen = Base64Encoding.encode(packet.length, 3); byte[] bufferLen;
byte[] buffer = ByteArrayUtils.combineByteArrays(bufferLen, packet);
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); return super.sendToStream(buffer);
} }
@Override
public void setRc4(RC4Cipher rc4) {
this.headerEncoder = rc4.deepCopy();
super.setRc4(rc4);
}
} }

View File

@ -1,11 +0,0 @@
package gearth.protocol.packethandler.shockwave.buffers;
import gearth.protocol.HPacket;
public interface ShockwaveBuffer {
void push(byte[] data);
HPacket[] receive();
}

View File

@ -1,44 +1,39 @@
package gearth.protocol.packethandler.shockwave.buffers; package gearth.protocol.packethandler.shockwave.buffers;
import gearth.protocol.HPacket; import gearth.protocol.crypto.RC4Cipher;
import gearth.protocol.packethandler.ByteArrayUtils; import gearth.protocol.packethandler.PayloadBuffer;
import gearth.protocol.packethandler.shockwave.packets.ShockPacket;
import gearth.protocol.packethandler.shockwave.packets.ShockPacketIncoming;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
public class ShockwaveInBuffer implements ShockwaveBuffer { public class ShockwaveInBuffer extends PayloadBuffer {
private byte[] buffer = new byte[0];
@Override @Override
public void push(byte[] data) { public void setCipher(RC4Cipher cipher) {
buffer = buffer.length == 0 ? data.clone() : ByteArrayUtils.combineByteArrays(buffer, data); // We don't need to decrypt incoming packet headers, for now.
} }
@Override @Override
public HPacket[] receive() { public byte[][] receive() {
if (buffer.length < 3) { if (buffer.length < 3) {
return new ShockPacket[0]; return new byte[0][];
} }
// Incoming packets are delimited by chr(1). // Incoming packets are delimited by chr(1).
// We need to split the buffer by chr(1) and then parse each packet. // We need to split the buffer by chr(1) and then parse each packet.
ArrayList<ShockPacket> packets = new ArrayList<>(); final ArrayList<byte[]> packets = new ArrayList<>();
int curPos = 0; int curPos = 0;
for (int i = 0; i < buffer.length; i++) { for (int i = 0; i < buffer.length; i++) {
if (buffer[i] == 1) { if (buffer[i] == 1) {
byte[] packetData = Arrays.copyOfRange(buffer, curPos, i); packets.add(Arrays.copyOfRange(buffer, curPos, i));
packets.add(new ShockPacketIncoming(packetData));
curPos = i + 1; curPos = i + 1;
} }
} }
buffer = Arrays.copyOfRange(buffer, curPos, buffer.length); buffer = Arrays.copyOfRange(buffer, curPos, buffer.length);
return packets.toArray(new ShockPacket[0]); return packets.toArray(new byte[0][]);
} }
} }

View File

@ -1,50 +1,65 @@
package gearth.protocol.packethandler.shockwave.buffers; package gearth.protocol.packethandler.shockwave.buffers;
import gearth.encoding.Base64Encoding; import gearth.encoding.Base64Encoding;
import gearth.protocol.HPacket; import gearth.protocol.crypto.RC4Cipher;
import gearth.protocol.packethandler.ByteArrayUtils; import gearth.protocol.packethandler.PayloadBuffer;
import gearth.protocol.packethandler.shockwave.packets.ShockPacket;
import gearth.protocol.packethandler.shockwave.packets.ShockPacketOutgoing;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
public class ShockwaveOutBuffer implements ShockwaveBuffer { public class ShockwaveOutBuffer extends PayloadBuffer {
private static final int PACKET_LENGTH_SIZE = 3; public static final int PACKET_HEADER_SIZE = 2;
private 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 @Override
public void push(byte[] data) { public void setCipher(RC4Cipher cipher) {
buffer = buffer.length == 0 ? data.clone() : ByteArrayUtils.combineByteArrays(buffer, data); this.cipher = cipher.deepCopy();
} }
@Override @Override
public HPacket[] receive() { public byte[][] receive() {
if (buffer.length < PACKET_SIZE_MIN) { final int packetLengthSize = this.cipher != null ? PACKET_LENGTH_SIZE_ENCRYPTED : PACKET_LENGTH_SIZE;
return new ShockPacket[0]; final int minPacketSize = this.cipher != null ? PACKET_SIZE_MIN_ENCRYPTED : PACKET_SIZE_MIN;
if (buffer.length < minPacketSize) {
return new byte[0][];
} }
ArrayList<ShockPacket> out = new ArrayList<>(); final ArrayList<byte[]> out = new ArrayList<>();
while (buffer.length >= PACKET_SIZE_MIN) { while (buffer.length >= minPacketSize) {
int length = Base64Encoding.decode(new byte[]{buffer[0], buffer[1], buffer[2]}); int length;
if (buffer.length < length + PACKET_LENGTH_SIZE) {
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; break;
} }
int endPos = length + PACKET_LENGTH_SIZE; int endPos = length + packetLengthSize;
byte[] packet = Arrays.copyOfRange(buffer, PACKET_LENGTH_SIZE, endPos);
out.add(new ShockPacketOutgoing(packet)); out.add(Arrays.copyOfRange(buffer, packetLengthSize, endPos));
buffer = Arrays.copyOfRange(buffer, endPos, buffer.length); buffer = Arrays.copyOfRange(buffer, endPos, buffer.length);
} }
return out.toArray(new ShockPacket[0]); return out.toArray(new byte[0][]);
} }
} }

View File

@ -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 <a href="https://github.com/aromaa">Joni</a> and <a href="https://github.com/scottstamp">DarkStar851</a> 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;
}
}

Binary file not shown.

View File

@ -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<RC4Cipher> cipher = new AtomicReference<>();
private final ShockwaveMemoryClient mockShockwaveMemoryClient = new ShockwaveMemoryClient(null) {
@Override
public List<byte[]> getRC4cached() {
return new ArrayList<>();
}
@Override
public List<byte[]> 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<HabboClientFactory> 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());
}
}