Support clientinitivexpand and not just expand2 for all TS3 server support (incl. TeaSpeak)
This commit is contained in:
+40
-12
@@ -17,6 +17,7 @@ import org.bouncycastle.math.ec.ECPoint;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Base64;
|
||||
import java.util.Random;
|
||||
@@ -58,13 +59,18 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
if (packet.getBody() instanceof PacketBody8Init1) {
|
||||
PacketBody8Init1 init1 = (PacketBody8Init1) packet.getBody();
|
||||
|
||||
Ts3Debugging.debug("Handle Init1 step " + init1.getStep().getNumber());
|
||||
|
||||
switch (init1.getStep().getNumber()) {
|
||||
case 1:
|
||||
PacketBody8Init1.Step1 serverReplyStep1 = (PacketBody8Init1.Step1)init1.getStep();
|
||||
|
||||
// Check nonce. It's received backwards, so walk backwards over the array received
|
||||
for (int i = 0; i < 4; i ++) {
|
||||
if (randomBytes[3 - i] != serverReplyStep1.getA0reversed()[i])
|
||||
throw new IllegalArgumentException("invalid server nonce");
|
||||
if (randomBytes[3 - i] != serverReplyStep1.getA0reversed()[i]) {
|
||||
Ts3Debugging.debug("[WARNING] random byte mismatch!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
@@ -138,6 +144,9 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
|
||||
sendInit1(response4);
|
||||
break;
|
||||
case 127:
|
||||
onAssigned();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unexpected Init1 server step: " + init1.getStep().getNumber());
|
||||
|
||||
@@ -145,7 +154,20 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
} else if (packet.getBody() instanceof PacketBody2Command) {
|
||||
SingleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplifyOne();
|
||||
|
||||
if (command.getName().equalsIgnoreCase("initivexpand2")) {
|
||||
Ts3Debugging.debug(command.build());
|
||||
|
||||
if (command.getName().equalsIgnoreCase("initivexpand")) {
|
||||
byte[] alpha = Base64.getDecoder().decode(command.get("alpha").getValue()); // alpha
|
||||
byte[] beta = Base64.getDecoder().decode(command.get("beta").getValue()); // beta
|
||||
byte[] omega = Base64.getDecoder().decode(command.get("omega").getValue()); // omega
|
||||
|
||||
getClient().setSecureParameters(Ts3Crypt.cryptoInit(alpha, beta, omega, getClient().getIdentity()));
|
||||
|
||||
sendClientInit();
|
||||
|
||||
getClient().setCommandProcessor(getClient());
|
||||
getClient().setState(ClientConnectionState.RETRIEVING_DATA);
|
||||
} else if (command.getName().equalsIgnoreCase("initivexpand2")) {
|
||||
// 3.2.2 initivexpand2 (Client <- Server)
|
||||
|
||||
if (!command.get("ot").getValue().equals("1"))
|
||||
@@ -190,6 +212,21 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
|
||||
alphaBytes = null;
|
||||
|
||||
sendClientInit();
|
||||
|
||||
getClient().setCommandProcessor(getClient());
|
||||
getClient().setState(ClientConnectionState.RETRIEVING_DATA);
|
||||
} else if (command.getName().equals("error")) {
|
||||
getClient().setState(ClientConnectionState.DISCONNECTED);
|
||||
|
||||
throw new IOException(command.get("msg").getValue());
|
||||
} else {
|
||||
throw new IOException("Unknown Init command: " + command.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendClientInit() throws IOException, TimeoutException {
|
||||
Command clientinit = new SingleCommand(
|
||||
"clientinit",
|
||||
ProtocolRole.CLIENT,
|
||||
@@ -232,14 +269,5 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
Ts3Debugging.debug(clientinit.build());
|
||||
|
||||
getClient().writePacket(new PacketBody2Command(ProtocolRole.CLIENT, clientinit));
|
||||
|
||||
getClient().setCommandProcessor(getClient());
|
||||
getClient().setState(ClientConnectionState.RETRIEVING_DATA);
|
||||
} else if (command.getName().equals("error")) {
|
||||
getClient().setState(ClientConnectionState.DISCONNECTED);
|
||||
|
||||
throw new IOException(command.get("msg").getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Punisher.NaCl.Internal.Ed25519Ref10.GroupElementP3;
|
||||
import Punisher.NaCl.Internal.Ed25519Ref10.GroupOperations;
|
||||
import Punisher.NaCl.Internal.Ed25519Ref10.ScalarOperations;
|
||||
import Punisher.NaCl.Internal.Sha512;
|
||||
import com.github.manevolent.ts3j.api.Message;
|
||||
import com.github.manevolent.ts3j.identity.*;
|
||||
import com.github.manevolent.ts3j.license.License;
|
||||
import com.github.manevolent.ts3j.util.Pair;
|
||||
@@ -24,6 +25,7 @@ import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.*;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
public final class Ts3Crypt {
|
||||
@@ -31,6 +33,16 @@ public final class Ts3Crypt {
|
||||
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public static byte[] hash128(byte[] data, int offs, int len) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
digest.update(data, offs, len);
|
||||
return digest.digest();
|
||||
} catch (GeneralSecurityException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] hash128(byte[] data) {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA1").digest(data);
|
||||
@@ -55,6 +67,52 @@ public final class Ts3Crypt {
|
||||
}
|
||||
}
|
||||
|
||||
public static Ts3Crypt.SecureChannelParameters cryptoInit(byte[] alpha, byte[] beta, byte[] omega,
|
||||
LocalIdentity identity)
|
||||
throws IOException {
|
||||
if (beta.length != 10 && beta.length != 54)
|
||||
throw new IOException("Invalid beta size: " + beta.length + " != (10,54)");
|
||||
|
||||
// getSharedSecret
|
||||
byte[] sharedKey = getSharedSecret(omega, identity);
|
||||
|
||||
// Splamy's setSharedSecret
|
||||
byte[] ivStruct = new byte[10 + beta.length];
|
||||
|
||||
//XorBinary(sharedKey, alpha, alpha.Length, ivStruct);
|
||||
xor(sharedKey, 0, alpha, 0, alpha.length, ivStruct, 0);
|
||||
|
||||
|
||||
//XorBinary(sharedKey.Slice(10), beta, beta.Length, ivStruct.AsSpan(10));
|
||||
xor(sharedKey, 10, beta, 0, beta.length, ivStruct, 10);
|
||||
|
||||
|
||||
byte[] buffer2 = hash128(ivStruct, 0, ivStruct.length);
|
||||
byte[] fakeSignature = new byte[8];
|
||||
System.arraycopy(buffer2, 0, fakeSignature, 0, 8);
|
||||
|
||||
return new SecureChannelParameters(fakeSignature, ivStruct);
|
||||
}
|
||||
|
||||
public static byte[] getSharedSecret(byte[] omega, LocalIdentity identity) {
|
||||
ECPoint publicKeyPoint = Ts3Crypt.decodePublicKey(omega);
|
||||
ECPoint p = publicKeyPoint.multiply(identity.getPrivateKey()).normalize();
|
||||
|
||||
byte[] keyArr = p.getAffineXCoord().toBigInteger().toByteArray();
|
||||
byte[] sharedSecret;
|
||||
if (keyArr.length == 32)
|
||||
sharedSecret = Ts3Crypt.hash128(keyArr);
|
||||
else if (keyArr.length > 32)
|
||||
sharedSecret = Ts3Crypt.hash128(keyArr, keyArr.length - 32, 32);
|
||||
else {
|
||||
byte[] keyArrExt = new byte[32];
|
||||
System.arraycopy(keyArr, 0, keyArrExt, 32 - keyArr.length, keyArr.length);
|
||||
sharedSecret = Ts3Crypt.hash128(keyArrExt);
|
||||
}
|
||||
|
||||
return sharedSecret;
|
||||
}
|
||||
|
||||
public static Ts3Crypt.SecureChannelParameters cryptoInit2(byte[] license, byte[] alpha,
|
||||
byte[] beta, byte[] privateKey) {
|
||||
// 3.2.2.2 Parsing the license
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.github.manevolent.ts3j;
|
||||
|
||||
import com.github.manevolent.ts3j.identity.LocalIdentity;
|
||||
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
||||
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||
import junit.framework.TestCase;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static com.github.manevolent.ts3j.util.Ts3Debugging.hexStringToByteArray;
|
||||
|
||||
/**
|
||||
* The assertion check values SHOULD be retrieved from another known working codebase (i.e. Splamy's C# codebase)
|
||||
* for the given input license string that is in no way associated with this codebase.
|
||||
*/
|
||||
public class CryptoInitTest extends TestCase {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new CryptoInitTest().testParser();
|
||||
}
|
||||
|
||||
public void testParser() throws Exception {
|
||||
Ts3Debugging.setEnabled(true);
|
||||
|
||||
LocalIdentity identity = LocalIdentity.load(
|
||||
new BigInteger(java.util.Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU="))
|
||||
);
|
||||
|
||||
byte[] alpha = Base64.decode("p0SD9rgLfAzMvQ==");
|
||||
byte[] beta = Base64.decode("gWw6y53Sft6JFA==");
|
||||
byte[] omega = Base64.decode(
|
||||
"MEwDAgcAAgEgAiAzliV+Hdt3q4PJ3G7+sA+lJdyz4pemWxwbZ" +
|
||||
"4VwUQtf3gIhAOIK7NxDEA4c54o8fu0mPjRShjWxQI6vW5ExD44JlPXb"
|
||||
);
|
||||
|
||||
assertEquals("wd/BG5pGG5zx05V7XDhtf+zhL38=", Base64.toBase64String(Ts3Crypt.getSharedSecret(omega, identity)));
|
||||
|
||||
Ts3Crypt.SecureChannelParameters parameters = Ts3Crypt.cryptoInit(
|
||||
alpha,
|
||||
beta,
|
||||
omega,
|
||||
identity
|
||||
);
|
||||
|
||||
|
||||
assertEquals("ZptC7SJNZ5A9bhQXZvPwrZI/pms=", Base64.toBase64String(parameters.getIvStruct()));
|
||||
|
||||
assertEquals("ZbeTrqtllww=", Base64.toBase64String(parameters.getFakeSignature()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class ServerConnectionTest {
|
||||
try {
|
||||
|
||||
client.connect(
|
||||
"teamlixo.net",
|
||||
new InetSocketAddress("178.32.109.167", 11079),
|
||||
null,
|
||||
10000L
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user