package com.steamcommunity.siplus.steamscreenshots;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.KeyFactory;
import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.LinkedList;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import android.os.SystemClock;
import com.google.common.io.LittleEndianDataInputStream;
import com.google.common.io.LittleEndianDataOutputStream;
import com.google.protobuf.InvalidProtocolBufferException;
import com.steamcommunity.siplus.steamscreenshots.proto.IncomingProtos.MultiProto;
public class Connection {
// THIS IS NOT A PERSISTENT CONNECTION!!!
// It doesn't send keep alive messages, so the app must send heartbeats (using sendHeartbeatIfNeeded) in other places.
// It sends keep alive messages when waiting for new messages, however.
// sendHeartbeatIfNeeded works only if needHeartbeat is set.
static final String CM_ADDRESS = "cm0.steampowered.com";
static final int CM_PORT = 27017;
static final byte[] ENCRYPT_RESPONSE_HEADER = {
-92, 0, 0, 0, // Size = 164
86, 84, 48, 49, // VT01
24, 5, 0, 0, // Type = ChannelEncryptResponse
-1, -1, -1, -1, -1, -1, -1, -1, // Target job ID = -1
-1, -1, -1, -1, -1, -1, -1, -1, // Source job ID = -1.
1, 0, 0, 0, // Protocol version = 1
-128, 0, 0, 0 // Key size = 128
};
static final byte[] RSA_KEY = {
48, -127, -99, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1,
5, 0, 3, -127, -117, 0, 48, -127, -121, 2, -127, -127, 0, -33, -20, 26,
-42, 44, 16, 102, 44, 23, 53, 58, 20, -80, 124, 89, 17, 127, -99, -45,
-40, 43, 122, -29, -32, 21, -51, 25, 30, 70, -24, 123, -121, 116, -94, 24,
70, 49, -87, 3, 20, 121, -126, -114, -23, 69, -94, 73, 18, -87, 35, 104,
115, -119, -49, 105, -95, -79, 97, 70, -67, -63, -66, -65, -42, 1, 27, -40,
-127, -44, -36, -112, -5, -2, 79, 82, 115, 102, -53, -107, 112, -41, -59, -114,
-70, 28, 122, 51, 117, -95, 98, 52, 70, -69, 96, -73, -128, 104, -6, 19,
-89, 122, -118, 55, 75, -98, -58, -12, 93, 95, 58, -103, -7, -98, -60, 58,
-23, 99, -94, -69, -120, 25, 40, -32, -25, 20, -64, 66, -119, 2, 1, 17
};
static final long TIMEOUT = 30000L;
static final int VT01 = 0x31305456;
Cipher mAES;
boolean mAESInitialized;
Cipher mAESIV;
SecretKeySpec mAESKeySpec;
ClientHeartBeatOutgoing mHeartbeat = new ClientHeartBeatOutgoing();
boolean mHeartbeatWhenWaiting;
LinkedList<byte[]> mMulti;
int mSessionID;
Socket mSocket;
long mSteamID = 0x110000100000000L;
void connect(String dstName, int dstPort) throws ConnectionException {
if (mSocket != null) {
try {
mSocket.close();
} catch (IOException e) {}
mSocket = null;
}
try {
mSocket = new Socket(dstName, dstPort);
} catch (UnknownHostException e) {
throw new ConnectionException();
} catch (IOException e) {
throw new ConnectionException();
}
mMulti = new LinkedList<byte[]>();
try {
mSocket.setSoTimeout(5000);
} catch (SocketException e) {
disconnectThrow();
}
IncomingData data;
try {
data = waitForMessage(ChannelEncryptRequestIncoming.MESSAGE);
if (data == null) {
disconnectThrow();
}
ChannelEncryptRequestIncoming requestMessage = new ChannelEncryptRequestIncoming(data);
if ((requestMessage.mProtocolVersion != 1) || (requestMessage.mUniverse != 1)) {
disconnectThrow();
}
} catch (IncomingException e) {
disconnectThrow();
}
try {
Cipher cipher = Cipher.getInstance("RSA/None/OAEPWithSHA1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, (RSAPublicKey)(KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(RSA_KEY))));
SecureRandom random = new SecureRandom();
byte[] aesKey = new byte[32];
random.nextBytes(aesKey);
byte[] encrypted = cipher.doFinal(aesKey);
CRC32 crc = new CRC32();
crc.update(encrypted);
long checksum = crc.getValue();
byte[] response = new byte[172];
System.arraycopy(ENCRYPT_RESPONSE_HEADER, 0, response, 0, 36);
System.arraycopy(encrypted, 0, response, 36, 128);
response[164] = (byte)(checksum & 255L);
response[165] = (byte)((checksum >> 8L) & 255L);
response[166] = (byte)((checksum >> 16L) & 255L);
response[167] = (byte)((checksum >> 24L) & 255L);
OutputStream stream = mSocket.getOutputStream();
stream.write(response);
stream.flush();
data = waitForMessage(ChannelEncryptResultIncoming.MESSAGE);
if (data == null) {
disconnectThrow();
}
ChannelEncryptResultIncoming resultMessage = new ChannelEncryptResultIncoming(data);
if (resultMessage.mEResult != EResult.OK) {
disconnectThrow();
}
mAES = Cipher.getInstance("AES/CBC/PKCS7Padding");
mAESIV = Cipher.getInstance("AES/ECB/NoPadding");
mAESKeySpec = new SecretKeySpec(aesKey, "AES");
mAESInitialized = true;
} catch (Exception e) {
disconnectThrow();
}
}
byte[] decryptMessage(byte[] data) throws IncomingException {
try {
mAESIV.init(Cipher.DECRYPT_MODE, mAESKeySpec);
mAES.init(Cipher.DECRYPT_MODE, mAESKeySpec, new IvParameterSpec(mAESIV.doFinal(data, 0, 16)));
return mAES.doFinal(data, 16, data.length - 16);
} catch (Exception e) {
disconnectThrowIncoming();
}
return null;
}
void disconnect() {
if (mSocket == null) {
return;
}
mMulti = null;
try {
mSocket.close();
} catch (IOException e) {}
mSocket = null;
}
void disconnectThrow() throws ConnectionException {
disconnect();
throw new ConnectionException();
}
void disconnectThrowIncoming() throws IncomingException {
disconnect();
throw new IncomingException();
}
void disconnectThrowOutgoing() throws OutgoingException {
disconnect();
throw new OutgoingException();
}
byte[] encryptMessage(int type, byte[] header, byte[] data, byte[] ivOutput) throws OutgoingException {
try {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);
mAES.init(Cipher.ENCRYPT_MODE, mAESKeySpec, new IvParameterSpec(iv));
mAESIV.init(Cipher.ENCRYPT_MODE, mAESKeySpec);
iv = mAESIV.doFinal(iv);
System.arraycopy(iv, 0, ivOutput, 0, iv.length);
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(8 + header.length + data.length);
@SuppressWarnings("resource")
LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(byteArrayStream);
stream.writeInt((int)(type | 0x80000000L));
stream.writeInt(header.length);
stream.write(header);
stream.write(data);
return mAES.doFinal(byteArrayStream.toByteArray());
} catch (Exception e) {
disconnectThrowOutgoing();
}
return null;
}
IncomingData nextMessage() throws IncomingException {
byte[] bytes;
if (mMulti.isEmpty()) {
bytes = requestNextMessage();
if (bytes == null) {
return null;
}
} else {
bytes = mMulti.remove(0);
}
IncomingData data;
try {
data = new IncomingData(bytes);
} catch (IncomingException e) {
disconnect();
throw e;
}
// Single message
if (data.mType != 1) {
return data;
}
// Multiple messages
if (!data.mProtobuf) {
disconnectThrowIncoming();
}
MultiProto proto = null;
try {
proto = MultiProto.parseFrom(data.mData);
} catch (InvalidProtocolBufferException e) {
disconnectThrowIncoming();
}
if (!proto.hasMessageBody()) {
return null;
}
byte[] multi = proto.getMessageBody().toByteArray();
int sizeUnzipped = proto.getSizeUnzipped();
if (sizeUnzipped != 0) {
if (sizeUnzipped < 0) {
disconnectThrowIncoming();
}
ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(multi));
try {
ZipEntry zipEntry = zipInputStream.getNextEntry();
if ((zipEntry == null) || (zipEntry.getSize() != sizeUnzipped)) {
return null;
}
multi = new byte[sizeUnzipped];
Utility.readFromStream(zipInputStream, multi);
} catch (IOException e) {
disconnectThrowIncoming();
}
}
LittleEndianDataInputStream stream = new LittleEndianDataInputStream(new ByteArrayInputStream(multi));
LinkedList<byte[]> newMultiMessages = new LinkedList<byte[]>();
try {
int multiLength;
byte[] multiMessage;
while (stream.available() > 4) {
multiLength = stream.readInt();
if ((multiLength < 5) || (multiLength > stream.available())) {
disconnectThrowIncoming();
}
multiMessage = new byte[multiLength];
Utility.readFromStream(stream, multiMessage);
newMultiMessages.add(multiMessage);
}
if (stream.available() != 0) {
disconnectThrowIncoming();
}
} catch (IOException e) {
disconnectThrowIncoming();
}
newMultiMessages.addAll(mMulti);
mMulti = newMultiMessages;
if (newMultiMessages.isEmpty()) {
return null;
}
bytes = newMultiMessages.get(0);
if ((bytes[0] == 1) && (bytes[1] == 0) && (bytes[2] == 0)) {
switch (bytes[3]) {
case 0:
disconnectThrowIncoming();
case -128:
return null;
}
}
newMultiMessages.remove(0);
try {
return new IncomingData(bytes);
} catch (IncomingException e) {
disconnect();
throw e;
}
}
byte[] requestNextMessage() throws IncomingException {
try {
InputStream stream = mSocket.getInputStream();
byte[] data = new byte[8];
Utility.readFromStream(stream, data);
int length = (data[0] & 255) | ((data[1] & 255) << 8) | ((data[2] & 255) << 16) | ((data[3] & 255) << 24);
if ((length < 16) || (length > 65527)) {
disconnectThrowIncoming();
}
if ((data[4] != 86) || (data[5] != 84) || (data[6] != 48) || (data[7] != 49)) {
disconnectThrowIncoming();
}
data = new byte[length];
Utility.readFromStream(stream, data);
if (mAESInitialized) {
data = decryptMessage(data);
}
return data;
} catch (InterruptedIOException e) {
return null;
} catch (IOException e) {
disconnectThrowIncoming();
}
return null;
}
void sendHeartbeat() {
MessageHeader header = mHeartbeat.mHeader;
header.mSessionID = mSessionID;
header.mSteamID = mSteamID;
try {
sendMessage(mHeartbeat);
} catch (OutgoingException e) {}
}
void sendMessage(Outgoing message) throws OutgoingException {
MessageHeader header = message.mHeader;
header.mSessionID = mSessionID;
header.mSteamID = mSteamID;
byte[] iv = new byte[16];
byte[] encrypted = encryptMessage(message.getMessageType(), header.serialize(), message.serialize(), iv);
try {
@SuppressWarnings("resource")
LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(mSocket.getOutputStream());
stream.writeInt(encrypted.length + 16);
stream.writeInt(VT01);
stream.write(iv);
stream.write(encrypted);
stream.flush();
} catch (IOException e) {
disconnectThrowOutgoing();
}
}
IncomingData waitForMessage(int message) throws IncomingException {
IncomingData data;
long start = SystemClock.elapsedRealtime();
for (;;) {
data = nextMessage();
if ((data != null) && (data.mType == message)) {
return data;
}
if ((SystemClock.elapsedRealtime() - start) > TIMEOUT) {
throw new IncomingException();
}
if (mHeartbeatWhenWaiting) {
sendHeartbeat();
}
}
}
}