Client authentication is done. It will now put an API client in a server, however we clearly need to work on decompression, and we also need to accept and handle ping packets appropriately.

Then we can start working on the text-based protocol Teamspeak uses.
This commit is contained in:
Manevolent
2018-07-27 16:07:29 -04:00
parent 146f2b6564
commit 6007b24b5f
16 changed files with 191 additions and 88 deletions
@@ -1058,26 +1058,14 @@ public class GroupOperations
public static void ge_p3_tobytes(byte[] s, int offset, GroupElementP3 h) public static void ge_p3_tobytes(byte[] s, int offset, GroupElementP3 h)
{ {
FieldElement recip = new FieldElement(); FieldElement recip = new FieldElement();
FieldElement x = new FieldElement(); FieldElement h1 = new FieldElement();
FieldElement y = new FieldElement(); FieldElement h2 = new FieldElement();
FieldOperations.fe_invert(recip, h.Z); FieldOperations.fe_invert(recip, h.Z);
FieldOperations.fe_mul(x, h.X, recip); FieldOperations.fe_mul(h1, h.X, recip);
FieldOperations.fe_mul(y, h.Y, recip); FieldOperations.fe_mul(h2, h.Y, recip);
FieldOperations.fe_tobytes(s, offset, y); FieldOperations.fe_tobytes(s, offset, h2);
}
public static void ge_p3_tobytes_negate(byte[] s, int offset, GroupElementP3 h) s[offset + 31] ^= (byte) (FieldOperations.fe_isnegative(h1) << 7);
{
FieldElement recip = new FieldElement();
FieldElement x = new FieldElement();
FieldElement y = new FieldElement();
FieldOperations.fe_invert(recip, h.Z);
FieldOperations.fe_mul(x, h.X, recip);
FieldOperations.fe_mul(y, h.Y, recip);
FieldOperations.fe_tobytes(s, offset, y);
s[offset + 31] ^= (byte) (FieldOperations.fe_isnegative(x) << 7);
} }
} }
@@ -28,7 +28,6 @@ public class Identity {
public Identity(ECPoint publicKey) { public Identity(ECPoint publicKey) {
this.publicKey = publicKey; this.publicKey = publicKey;
this.publicKeyString = Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(publicKey)); this.publicKeyString = Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(publicKey));
this.fingerprint = generateFingerprint(); this.fingerprint = generateFingerprint();
} }
@@ -98,7 +97,7 @@ public class Identity {
int numLen = 0; int numLen = 0;
do do
{ {
numBuffer[numLen] = (byte)((offset % 10) & 0xFF); numBuffer[numLen] = (byte)(48 + (offset % 10));
offset /= 10; offset /= 10;
numLen ++; numLen ++;
} while (offset > 0); } while (offset > 0);
@@ -132,7 +131,9 @@ public class Identity {
private String generateFingerprint() { private String generateFingerprint() {
synchronized (sha1) { synchronized (sha1) {
return Base64.getEncoder().encodeToString(sha1.digest(Ts3Crypt.encodePublicKey(publicKey))); return Base64.getEncoder().encodeToString(
sha1.digest(getPublicKeyString().getBytes(Charset.forName("ASCII")))
);
} }
} }
} }
@@ -24,15 +24,12 @@ public class LocalIdentity extends Identity {
} }
public static LocalIdentity load(ECPoint publicKey, public static LocalIdentity load(ECPoint publicKey,
BigInteger privateKey, BigInteger privateKey) {
long keyOffset, return new LocalIdentity(publicKey, privateKey);
long lastCheckedKeyOffset) { }
LocalIdentity localIdentity = new LocalIdentity(publicKey, privateKey);
localIdentity.setKeyOffset(keyOffset); public static LocalIdentity load(BigInteger privateKey) {
localIdentity.setLastCheckedKeyOffset(lastCheckedKeyOffset); return new LocalIdentity(Ts3Crypt.generatePublicKeyFromPrivateKey(privateKey), privateKey);
return localIdentity;
} }
public static LocalIdentity generateNew(int securityLevel) throws GeneralSecurityException { public static LocalIdentity generateNew(int securityLevel) throws GeneralSecurityException {
@@ -48,7 +45,7 @@ public class LocalIdentity extends Identity {
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate(); ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic(); ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
LocalIdentity localIdentity = load(publicKey.getQ().normalize(), privateKey.getD(), 0, 0); LocalIdentity localIdentity = load(publicKey.getQ().normalize(), privateKey.getD());
localIdentity.improveSecurity(securityLevel); localIdentity.improveSecurity(securityLevel);
return localIdentity; return localIdentity;
@@ -218,6 +218,7 @@ public class License {
byte[] final_ = new byte[32]; byte[] final_ = new byte[32];
GroupOperations.ge_p3_tobytes(final_, 0, r2); GroupOperations.ge_p3_tobytes(final_, 0, r2);
final_[31] ^= 0x80;
return final_; return final_;
} }
@@ -4,6 +4,7 @@ import com.github.manevolent.ts3j.command.SimpleCommand;
import com.github.manevolent.ts3j.command.part.CommandSingleParameter; import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
import com.github.manevolent.ts3j.protocol.Packet; import com.github.manevolent.ts3j.protocol.Packet;
import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.ProtocolRole;
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command; import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
import com.github.manevolent.ts3j.protocol.packet.PacketBody8Init1; import com.github.manevolent.ts3j.protocol.packet.PacketBody8Init1;
import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket;
@@ -45,11 +46,6 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
} }
private void sendInit1(PacketBody8Init1 packet) throws IOException, TimeoutException { private void sendInit1(PacketBody8Init1 packet) throws IOException, TimeoutException {
Ts3Debugging.debug("Connecting: sending " + packet.getClass().getSimpleName()
+ ":"
+ packet.getStep().getClass().getSimpleName() + "..."
);
packet.setVersion(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF }); packet.setVersion(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF });
getClient().writePacket(packet); getClient().writePacket(packet);
@@ -57,8 +53,6 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
@Override @Override
public void handlePacket(Packet packet) throws IOException, TimeoutException { public void handlePacket(Packet packet) throws IOException, TimeoutException {
Ts3Debugging.debug("Connecting: handling " + packet.getBody().getClass().getSimpleName());
if (packet.getBody() instanceof PacketBody8Init1) { if (packet.getBody() instanceof PacketBody8Init1) {
PacketBody8Init1 init1 = (PacketBody8Init1) packet.getBody(); PacketBody8Init1 init1 = (PacketBody8Init1) packet.getBody();
@@ -101,8 +95,6 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
byte[] solution = byte[] solution =
x.modPow(BigInteger.valueOf(2L).pow(serverReplyStep3.getLevel()), n).toByteArray(); x.modPow(BigInteger.valueOf(2L).pow(serverReplyStep3.getLevel()), n).toByteArray();
Ts3Debugging.debug(Ts3Debugging.getHex(solution));
System.arraycopy( System.arraycopy(
solution, Math.abs(solution.length - 64), solution, Math.abs(solution.length - 64),
y, solution.length < 64 ? 64 - solution.length : 0, y, solution.length < 64 ? 64 - solution.length : 0,
@@ -187,8 +179,6 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
getClient().getIdentity() getClient().getIdentity()
); );
Ts3Debugging.debug("Sending clientek...");
getClient().sendCommand( getClient().sendCommand(
new SimpleCommand( new SimpleCommand(
"clientek", ProtocolRole.CLIENT, "clientek", ProtocolRole.CLIENT,
@@ -199,36 +189,52 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue())); getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue()));
Ts3Debugging.debug("Sending clientinit...");
getClient().sendCommand(new SimpleCommand( getClient().sendCommand(new SimpleCommand(
"clientinit", "clientinit",
ProtocolRole.CLIENT, ProtocolRole.CLIENT,
new CommandSingleParameter("client_nickname", "test"), new CommandSingleParameter("client_nickname", getClient().getNickname()),
new CommandSingleParameter("client_version", "3.1 [Build: 1471417187]"), new CommandSingleParameter("client_version", "3.1.8 [Build: 1516614607]"),
new CommandSingleParameter("client_platform", "Windows"), new CommandSingleParameter("client_platform", "Windows"),
new CommandSingleParameter( new CommandSingleParameter(
"client_version_sign", "client_version_sign",
"gDEgQf/BiOQZdAheKccM1XWcMUj2OUQqt75oFuvF2c0MQMXyv88cZQdUuckKbcBRp7RpmLInto4PIgd7mPO7BQ==" "LJ5q+KWT4KwBX7oR/9j9A12hBrq5ds5ony99" +
"f9kepNmqFskhT7gfB51bAJNgAMOzXVCeaItNmc10F2wUNktqCw=="
), ),
new CommandSingleParameter("client_input_hardware", "1"), new CommandSingleParameter("client_input_hardware", "1"),
new CommandSingleParameter("client_output_hardware", "1"), new CommandSingleParameter("client_output_hardware", "1"),
new CommandSingleParameter("client_default_channel", ""), new CommandSingleParameter("client_default_channel",
new CommandSingleParameter("client_default_channel_password", ""), getClient().getOption("client.default_channel", String.class)
new CommandSingleParameter("client_server_password", ""), ),
new CommandSingleParameter("client_nickname_phonetic", "test"), new CommandSingleParameter("client_default_channel_password",
getClient().getOption("client.default_channel_password", String.class)
),
new CommandSingleParameter(
"client_server_password",
getClient().getOption("client.server_password", String.class)
),
new CommandSingleParameter("client_nickname_phonetic",
getClient().getOption("client.nickname_phonetic", String.class)
),
new CommandSingleParameter("client_meta_data", ""), new CommandSingleParameter("client_meta_data", ""),
new CommandSingleParameter("client_default_token", ""), new CommandSingleParameter("client_default_token",
getClient().getOption("client.default_token", String.class)
),
new CommandSingleParameter( new CommandSingleParameter(
"client_key_offset", "client_key_offset",
Long.toString(getClient().getIdentity().getKeyOffset()) Long.toString(getClient().getIdentity().getKeyOffset())
), ),
new CommandSingleParameter( new CommandSingleParameter(
"hwid", "hwid",
"+LyYqbDqOvJJpN5pdAbF8/v5kZ0=" getClient().getOption("client.hwid", String.class) != null ?
getClient().getOption("client.hwid", String.class) :
"+LyYqbDqOvEEpN5pdAbF8/v5kZ0="
) )
) )
); );
} else if (command.getName().equals("error")) {
getClient().setState(ClientConnectionState.DISCONNECTED);
throw new IOException(command.get("msg").getValue());
} }
} }
} }
@@ -327,15 +327,13 @@ public abstract class AbstractTeamspeakClientSocket
packet.getHeader().getPacketId(), packet.getHeader().getPacketId(),
response = new PacketResponse( response = new PacketResponse(
networkPacket, networkPacket,
Integer.MAX_VALUE networkPacket.getHeader().getType().canResend() ? Integer.MAX_VALUE : 0
) )
); );
else else
response = null; response = null;
Ts3Debugging.debug("[PROTOCOL] WRITE " + networkPacket.getHeader().getType().name()); Ts3Debugging.debug("[PROTOCOL] WRITE " + networkPacket.getHeader().getType().name());
for (HeaderFlag flag : HeaderFlag.values())
Ts3Debugging.debug(flag.name() + ": " + networkPacket.getHeader().getPacketFlag(flag));
writeNetworkPacket(networkPacket); writeNetworkPacket(networkPacket);
@@ -350,7 +348,10 @@ public abstract class AbstractTeamspeakClientSocket
response.waitForAcknowledgement(1000); response.waitForAcknowledgement(1000);
break; break;
} catch (TimeoutException e) { } catch (TimeoutException e) {
if (response.willResend())
response.resend(); response.resend();
else
throw e;
} catch (CancellationException e) { } catch (CancellationException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@@ -398,11 +399,6 @@ public abstract class AbstractTeamspeakClientSocket
); );
} }
Ts3Debugging.debug("[NETWORK] READ " +
packet.getHeader().getType() + " BODY "
+ Ts3Debugging.getHex(packetBuffer.array())
);
// Fragment handling // Fragment handling
if (fragment) { if (fragment) {
Ts3Debugging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name() + " fragment"); Ts3Debugging.debug("[PROTOCOL] READ " + networkPacket.getHeader().getType().name() + " fragment");
@@ -547,6 +543,14 @@ public abstract class AbstractTeamspeakClientSocket
this.generationId = generationId; this.generationId = generationId;
} }
public String getNickname( ){
return getOption("client.nickname", String.class);
}
public void setNickname(String name) {
setOption("client.nickname", name);
}
private class NetworkHandler implements Runnable { private class NetworkHandler implements Runnable {
@Override @Override
public void run() { public void run() {
@@ -554,9 +558,10 @@ public abstract class AbstractTeamspeakClientSocket
try { try {
Packet packet = readQueue.take(); Packet packet = readQueue.take();
Ts3Debugging.debug("Processing " + packet + "..."); // separate these so we getHandler at proper runtime instant rather than get it and let it change
// on us
getHandler().handlePacket(packet); getHandler().handlePacket(packet);
Ts3Debugging.debug("Processed " + packet + ".");
} catch (Exception e) { } catch (Exception e) {
if (e instanceof InterruptedException) continue; if (e instanceof InterruptedException) continue;
@@ -605,12 +610,25 @@ public abstract class AbstractTeamspeakClientSocket
tries ++; tries ++;
} }
/**
* Waits for a package acknowledgement.
* @return true if the acknowledgement wasn't null (shouldn't ever happen)
* @throws ExecutionException
* @throws InterruptedException
*/
public boolean waitForAcknowledgement() public boolean waitForAcknowledgement()
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
Packet acknowledgement = future.get(); Packet acknowledgement = future.get();
return acknowledgement != null; return acknowledgement != null;
} }
/**
* Waits for a package acknowledgement.
* @param millis Milliseconds to wait. When transpired with no completion, TimeoutException is thrown.
* @return true if the acknowledgement wasn't null (shouldn't ever happen)
* @throws ExecutionException
* @throws InterruptedException
*/
public boolean waitForAcknowledgement(long millis) public boolean waitForAcknowledgement(long millis)
throws TimeoutException, ExecutionException, InterruptedException { throws TimeoutException, ExecutionException, InterruptedException {
Packet acknowledgement = future.get(millis, TimeUnit.MILLISECONDS); Packet acknowledgement = future.get(millis, TimeUnit.MILLISECONDS);
@@ -618,7 +636,7 @@ public abstract class AbstractTeamspeakClientSocket
return acknowledgement != null; return acknowledgement != null;
} }
public boolean isWillResend() { public boolean willResend() {
return willResend; return willResend;
} }
@@ -65,7 +65,6 @@ public class LocalTeamspeakClientSocket extends AbstractTeamspeakClientSocket {
packet.getDatagram().getLength() packet.getDatagram().getLength()
+ " to " + + " to " +
socket.getRemoteSocketAddress()); socket.getRemoteSocketAddress());
Ts3Debugging.debug("[NETWORK] WRITE " + Ts3Debugging.getHex(packet.getBuffer().array()));
socket.send(packet.getDatagram()); socket.send(packet.getDatagram());
} }
@@ -19,7 +19,7 @@ public interface TeamspeakClientSocket extends TeamspeakSocket {
default <T extends Object> T getOption(String key, Class<T> clazz) { default <T extends Object> T getOption(String key, Class<T> clazz) {
Object value = getOptions().get(key); Object value = getOptions().get(key);
if (value == null) return (T) null; if (value == null) return null;
else if (!value.getClass().isAssignableFrom(clazz)) { else if (!value.getClass().isAssignableFrom(clazz)) {
throw new ClassCastException( throw new ClassCastException(
@@ -226,11 +226,15 @@ public final class Ts3Crypt {
GroupOperations.ge_scalarmult_base(A, privateKey, 0); GroupOperations.ge_scalarmult_base(A, privateKey, 0);
byte[] publicKey = new byte[32]; byte[] publicKey = new byte[32];
GroupOperations.ge_p3_tobytes_negate(publicKey, 0, A); GroupOperations.ge_p3_tobytes(publicKey, 0, A);
return new Pair<>(publicKey, privateKey); return new Pair<>(publicKey, privateKey);
} }
public static ECPoint generatePublicKeyFromPrivateKey(BigInteger privateKey) {
return ECNamedCurveTable.getParameterSpec("prime256v1").getG().multiply(privateKey).normalize();
}
public static final class SecureChannelParameters { public static final class SecureChannelParameters {
private final byte[] fakeSignature; private final byte[] fakeSignature;
private final byte[] ivStruct; private final byte[] ivStruct;
@@ -41,7 +41,7 @@ public final class Ts3String {
case '/': sb.append('/'); break; case '/': sb.append('/'); break;
case '\\': sb.append('\\'); break; case '\\': sb.append('\\'); break;
default: default:
throw new IllegalArgumentException("invalid escape code: " + c); throw new IllegalArgumentException("invalid escape code: " + string.charAt(i) + " (" + Character.getName(string.charAt(i)) + ")");
} }
} else sb.append(c); } else sb.append(c);
} }
@@ -8,6 +8,10 @@ import junit.framework.TestCase;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Base64; import java.util.Base64;
/**
* This is a self proof to make sure we can encrypt and decrypt our OWN data. This does not prove that the encrypted
* data would be correct. For that, there are other tests.
*/
public class ClientEkTest extends TestCase { public class ClientEkTest extends TestCase {
public static byte[] hexStringToByteArray(String s) { public static byte[] hexStringToByteArray(String s) {
int len = s.length(); int len = s.length();
@@ -25,15 +29,14 @@ public class ClientEkTest extends TestCase {
public void testParser() throws Exception { public void testParser() throws Exception {
LocalIdentity keyPair = LocalIdentity.load( LocalIdentity keyPair = LocalIdentity.load(
Ts3Crypt.decodePublicKey(Base64.getDecoder() new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJ" +
.decode("MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLbKYSpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=")), "nRUl4auQf8+znjYrOmWU="))
new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU=")),
2294,
2295
); );
byte[] key = hexStringToByteArray("5763FA00EBC38DCE3953AFCACD8CFF2209208D15BB7D9B699D61A5675EDA9370"); byte[] key = hexStringToByteArray("5763FA00EBC38DCE3953AFCACD8CFF2209208D15BB7D" +
byte[] beta = hexStringToByteArray("EA581E7CBADED7C47B9CBA3D3EEFB33A49B61ED0F0B45DF50D900C12F551B5F6B0EFEE3F593FB225184C78C06DB2D688669B70ABAC6D"); "9B699D61A5675EDA9370");
byte[] beta = hexStringToByteArray("EA581E7CBADED7C47B9CBA3D3EEFB33A49B61ED0F0B45DF" +
"50D900C12F551B5F6B0EFEE3F593FB225184C78C06DB2D688669B70ABAC6D");
byte[] signature = Ts3Crypt.generateClientEkProof(key, beta, keyPair.getPrivateKey()); byte[] signature = Ts3Crypt.generateClientEkProof(key, beta, keyPair.getPrivateKey());
@@ -0,0 +1,37 @@
package com.github.manevolent.ts3j;
import com.github.manevolent.ts3j.identity.LocalIdentity;
import com.github.manevolent.ts3j.util.Ts3Crypt;
import junit.framework.TestCase;
import java.math.BigInteger;
import java.util.Base64;
public class ClientUidGenerationTest extends TestCase {
public static void main(String[] args) throws Exception {
new ClientUidGenerationTest().testParser();
}
public void testParser() throws Exception {
LocalIdentity keyPair = LocalIdentity.load(
new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU="))
);
keyPair.improveSecurity(10);
assertEquals(2294, keyPair.getKeyOffset());
assertEquals(2295, keyPair.getLastCheckedKeyOffset());
assertEquals("MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLb" +
"KYSpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=", keyPair.getPublicKeyString());
assertEquals("27QHSPRzQe+enxh56eUokkLmsFg=", keyPair.getFingerprint());
assertEquals(
"MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLbKY" +
"SpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=",
keyPair.getPublicKeyString());
}
}
@@ -6,6 +6,10 @@ import junit.framework.TestCase;
import static com.github.manevolent.ts3j.util.Ts3Debugging.hexStringToByteArray; 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 CryptoInit2Test extends TestCase { public class CryptoInit2Test extends TestCase {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
@@ -13,15 +17,20 @@ public class CryptoInit2Test extends TestCase {
} }
public void testParser() throws Exception { public void testParser() throws Exception {
byte[] licenseBytes = hexStringToByteArray("0100358541498A24ACD30157918B8F50955C0DAE970AB65372CBE407415FCF3E029B02084D15E00AA793600700000020416E6F6E796D6F7573000047D9E4DC25AA2E90ACD4DB5FA61C8FED369B346D84C2CA2FCCCA86F73AFEF092200A77C8810A787141"); byte[] licenseBytes = hexStringToByteArray("0100358541498A24ACD30157918B8F50955C0DAE970AB65372CBE407" +
"415FCF3E029B02084D15E00AA793600700000020416E6F6E796D6F7573000047D9E4DC25AA2E90ACD4DB5FA61C8F" +
"ED369B346D84C2CA2FCCCA86F73AFEF092200A77C8810A787141");
byte[] alpha = hexStringToByteArray("9500A5DB3B50ACECAB81"); byte[] alpha = hexStringToByteArray("9500A5DB3B50ACECAB81");
byte[] beta = hexStringToByteArray("EAFFC9A8BC996B25C8AA700264E99E372ECCDEB1C121D6EC0F4D49FB46CEEBA4E3C724B3070FD70CB03D7BC08129205690ECE228CA7C"); byte[] beta = hexStringToByteArray("EAFFC9A8BC996B25C8AA700264E99E372ECCDEB1C121D6EC0F4D49FB46" +
byte[] privateKey = hexStringToByteArray("102E591ABA4508129E812FF3437E2DDD3CA1F1EC341117CA3514CC347A7C2A77"); "CEEBA4E3C724B3070FD70CB03D7BC08129205690ECE228CA7C");
byte[] privateKey = hexStringToByteArray("102E591ABA4508129E812FF3437E2DDD3CA1F1EC341117CA35" +
"14CC347A7C2A77");
Ts3Crypt.SecureChannelParameters result = Ts3Crypt.SecureChannelParameters result =
Ts3Crypt.cryptoInit2(licenseBytes, alpha, beta, privateKey); Ts3Crypt.cryptoInit2(licenseBytes, alpha, beta, privateKey);
assertEquals(Ts3Debugging.getHex(result.getIvStruct()), "E4082A92F71C96A947452F5582EF2879B2051ED2D3F2C6B0643CF5A266EE6B5180573C2F5F3F1C4AC579188366F16AE0EADC3AAF860805D8F2A831E9E49F4513"); assertEquals(Ts3Debugging.getHex(result.getIvStruct()), "E4082A92F71C96A947452F5582EF2879B2051ED2D3" +
"F2C6B0643CF5A266EE6B5180573C2F5F3F1C4AC579188366F16AE0EADC3AAF860805D8F2A831E9E49F4513");
assertEquals(Ts3Debugging.getHex(result.getFakeSignature()), "54F2B4D661E0F9AB"); assertEquals(Ts3Debugging.getHex(result.getFakeSignature()), "54F2B4D661E0F9AB");
} }
@@ -5,7 +5,17 @@ import com.github.manevolent.ts3j.util.Ts3Debugging;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Base64;
/**
* Tests license derivation. Licenses are part of generating a shared key. The seed for the randomness is an
* ephemeral license at the tail end of a license block. That jumbles up the registers in the hash of the block,
* which is used in the protocol to generate the shared AES keys. What we're doing here is making sure that derivation
* happens appropriately and continues to in the future.
*
* 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 LicenseDerivationTest extends TestCase { public class LicenseDerivationTest extends TestCase {
public static byte[] hexStringToByteArray(String s) { public static byte[] hexStringToByteArray(String s) {
int len = s.length(); int len = s.length();
@@ -21,11 +31,38 @@ public class LicenseDerivationTest extends TestCase {
new LicenseDerivationTest().testParser(); new LicenseDerivationTest().testParser();
} }
public void testParser() throws Exception { private void check(String base64License, String expectedResult) {
byte[] licenseBytes = hexStringToByteArray("0100358541498A24ACD30157918B8F50955C0DAE970AB65372CBE407415FCF3E029B02084D15E00AA793600700000020416E6F6E796D6F75730000AA43CD339C6E30E4E848F2FF031B93146B2E4925B28B33A3E8054D2101D06FF7200A776C5E0A78151E"); assertEquals(Base64.getEncoder().encodeToString(
byte[] derrivedKey = License.deriveKey(License.readLicenses(ByteBuffer.wrap(licenseBytes))); License.deriveKey(License.readLicenses(ByteBuffer.wrap(
assertEquals(Ts3Debugging.getHex(derrivedKey), "B4EBE337EA2AA9F37C3E2FF8889D24B08B7439E654513B8BF342D1D174895C30"); Base64.getDecoder().decode(base64License)
)))
), expectedResult);
}
public void testParser() throws Exception {
check("AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4CmwIITRXgCqeTYA" +
"cAAAAgQW5vbnltb3VzAADSN9wlGHZEHZvX7ImHoqYezibj5byDh0f4oMsG3afDxyAKePI" +
"VCnma1Q==",
"z/bYm6TmHmuAil/osx8eGi6Oits2vIO4i6Bm13RuiGg=");
check("AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4C" +
"mwIITRXgCqeTYAcAAAAgQW5vbnltb3VzAABx1YQfzCiB8b" +
"ZZAdGwXNTLmdhiOpjaH3OOlISy5vrM3iAKePBVCnmZFQ==",
"lrukIi392D7ltdKFp5mURT3Ydk+oWYNjMt3kptbQl6I=");
check("AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4CmwIITR" +
"XgCqeTYAcAAAAgQW5vbnltb3VzAAAK5C0l+xtOTAZGEA/GHHOySAUEBmq7fN5" +
"PG7uSGPEADiAKePGHCnmaRw==",
"H+UcEreBUkCWN18nTYZp0QQkQqGA8IqzqvJ5qB225Z8=");
check("AQCvbHFTQDY/terPeilrp/ECU9xCH5U3xC92lYTNaY/0KQ" +
"AJFueAazbsgAAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAACOaeEfqmGhp2qXAO" +
"QS7IfKw939kEy1i6NSE0OsO4IIPgAJRa7CL1XtIQAAACRUZWFtU3BlYWsgc3lzdGVtcy" +
"BHbWJIAACvbItqlhq4qOHjWFNky0ASbs2BBL//QMVspoGLWmXhzQIJSQCAH" +
"/mxgAYAAAAAVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAAWysE+/5ug1yKk4WiiWMNeB+" +
"E+vQ5RmxglJLi8rW15UsgCnjyQAp5mwA=",
"ZWGGM5QmRdl6AQOgBUZgOAVHcicsm/YPYR3EukrFiGE=");
} }
} }
@@ -9,6 +9,9 @@ import org.bouncycastle.math.ec.ECPoint;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Base64; import java.util.Base64;
/**
* Public keys get encoded to base 64 and get passed around a lot in Teamspeak, especially early on in the protocol.
*/
public class PublicKeyEncodingTest extends TestCase { public class PublicKeyEncodingTest extends TestCase {
public static byte[] hexStringToByteArray(String s) { public static byte[] hexStringToByteArray(String s) {
int len = s.length(); int len = s.length();
@@ -26,11 +29,7 @@ public class PublicKeyEncodingTest extends TestCase {
public void testParser() throws Exception { public void testParser() throws Exception {
LocalIdentity keyPair = LocalIdentity.load( LocalIdentity keyPair = LocalIdentity.load(
Ts3Crypt.decodePublicKey(Base64.getDecoder() new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU="))
.decode("MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLbKYSpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=")),
new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU=")),
2294,
2295
); );
ECPoint point = Ts3Crypt.decodePublicKey(Base64.getDecoder().decode(keyPair.getPublicKeyString())); ECPoint point = Ts3Crypt.decodePublicKey(Base64.getDecoder().decode(keyPair.getPublicKeyString()));
@@ -28,6 +28,10 @@ public class ServerConnectionTest {
client.setIdentity(localIdentity); client.setIdentity(localIdentity);
client.setNickname("Hello from Java");
client.setOption("client.hwid", "JAVAJAVAJAVA");
client.connect(new InetSocketAddress( client.connect(new InetSocketAddress(
"ts.teamlixo.net", "ts.teamlixo.net",
9987), 9987),