From 4a5bdcd03f3d39a06184f32761cc256d405d0ba0 Mon Sep 17 00:00:00 2001 From: Manevolent Date: Tue, 24 Jul 2018 16:23:10 -0400 Subject: [PATCH] We're now up to the point where we need to work with EdDSA. More to come --- .../manevolent/ts3j/command/Commands.java | 20 -- .../ts3j/command/SimpleCommand.java | 74 ++++++- .../manevolent/ts3j/command/TypedCommand.java | 60 ------ .../ts3j/command/part/CommandOption.java | 5 + .../ts3j/command/part/CommandParameter.java | 8 + .../command/part/CommandSingleParameter.java | 10 + .../command/type/client/ClientInitIV.java | 16 -- .../type/server/ServerInitIVExpand.java | 15 -- .../type/server/ServerInitIVExpand2.java | 18 -- .../manevolent/ts3j/identity/Identity.java | 20 +- .../ts3j/license/EphemeralLicenseUse.java | 36 ++++ .../ts3j/license/IntermediateLicenseUse.java | 65 +++++++ .../manevolent/ts3j/license/License.java | 182 ++++++++++++++++++ .../manevolent/ts3j/license/LicenseUse.java | 20 ++ .../ts3j/license/LicenseUseType.java | 31 +++ .../ts3j/license/ServerLicenseUse.java | 71 +++++++ .../ts3j/protocol/packet/Packet2Command.java | 5 + .../client/LocalClientHandlerConnecting.java | 69 ++++++- .../protocol/socket/LocalTeamspeakSocket.java | 10 +- .../github/manevolent/ts3j/util/Ts3Crypt.java | 82 ++++++++ .../manevolent/ts3j/util/Ts3String.java | 2 +- .../manevolent/ts3j/ServerConnectionTest.java | 2 +- 22 files changed, 655 insertions(+), 166 deletions(-) delete mode 100644 src/main/java/com/github/manevolent/ts3j/command/Commands.java delete mode 100644 src/main/java/com/github/manevolent/ts3j/command/TypedCommand.java delete mode 100644 src/main/java/com/github/manevolent/ts3j/command/type/client/ClientInitIV.java delete mode 100644 src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand.java delete mode 100644 src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand2.java create mode 100644 src/main/java/com/github/manevolent/ts3j/license/EphemeralLicenseUse.java create mode 100644 src/main/java/com/github/manevolent/ts3j/license/IntermediateLicenseUse.java create mode 100644 src/main/java/com/github/manevolent/ts3j/license/License.java create mode 100644 src/main/java/com/github/manevolent/ts3j/license/LicenseUse.java create mode 100644 src/main/java/com/github/manevolent/ts3j/license/LicenseUseType.java create mode 100644 src/main/java/com/github/manevolent/ts3j/license/ServerLicenseUse.java create mode 100644 src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java diff --git a/src/main/java/com/github/manevolent/ts3j/command/Commands.java b/src/main/java/com/github/manevolent/ts3j/command/Commands.java deleted file mode 100644 index d54e966..0000000 --- a/src/main/java/com/github/manevolent/ts3j/command/Commands.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.manevolent.ts3j.command; - -import com.github.manevolent.ts3j.command.type.client.*; -import com.github.manevolent.ts3j.command.type.server.*; - -import java.util.LinkedHashMap; -import java.util.Map; - -public final class Commands { - private static final Map> commands = new LinkedHashMap<>(); - - static { - // Client - commands.put("clientinitiv", ClientInitIV.class); - - // Server - commands.put("initivexpand", ServerInitIVExpand.class); - commands.put("initivexpand2", ServerInitIVExpand2.class); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java b/src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java index 54156d3..7dc422e 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java +++ b/src/main/java/com/github/manevolent/ts3j/command/SimpleCommand.java @@ -1,20 +1,30 @@ package com.github.manevolent.ts3j.command; +import com.github.manevolent.ts3j.command.part.CommandOption; import com.github.manevolent.ts3j.command.part.CommandParameter; +import com.github.manevolent.ts3j.command.part.CommandSingleParameter; import com.github.manevolent.ts3j.protocol.ProtocolRole; +import com.github.manevolent.ts3j.util.Ts3Logging; +import com.github.manevolent.ts3j.util.Ts3String; -import java.util.Collection; -import java.util.Collections; +import java.util.*; public class SimpleCommand implements Command { private final String name; private final ProtocolRole role; - private final Collection params; + private final Map params; - public SimpleCommand(String name, ProtocolRole role, Collection params) { + public SimpleCommand(String name, ProtocolRole role, CommandParameter... params) { + this(name, role, Arrays.asList(params)); + } + + public SimpleCommand(String name, ProtocolRole role, List params) { this.name = name; this.role = role; - this.params = params; + + this.params = new LinkedHashMap<>(); + + for (CommandParameter parameter : params) add(parameter); } @Override @@ -29,6 +39,58 @@ public class SimpleCommand implements Command { @Override public Iterable getParameters() { - return Collections.unmodifiableCollection(params); + return Collections.unmodifiableCollection(params.values()); + } + + public boolean has(String key) { + return params.containsKey(key.toLowerCase()); + } + + public void add(CommandParameter parameter) { + CommandParameter existing = get(parameter.getName()); + if (existing != null) throw new IllegalArgumentException(parameter.getName() + " already set"); + + if (parameter.getName().trim().length() <= 0) + throw new IllegalArgumentException("invalid key (empty)"); + + this.params.put(parameter.getName().toLowerCase(), parameter); + } + + public CommandParameter get(String key) { + return this.params.get(key.toLowerCase()); + } + + public CommandParameter remove(String key) { + return this.params.remove(key); + } + + public static SimpleCommand parse(ProtocolRole role, String commandText) { + String[] commandParts = commandText.split(" "); + + String commandLabel = commandParts[0].toLowerCase(); + + List parameterList = new LinkedList<>(); + + for (int i = 1; i < commandParts.length; i ++) { + String commandParamText = commandParts[i].trim(); + if (commandParamText.length() <= 0) continue; + + if (commandParamText.startsWith("-")) { // Option + parameterList.add(new CommandOption(commandParamText.substring(1))); + } else { + String[] commandParamParts = commandParamText.split("=", 2); + + Ts3Logging.debug(commandParamText); + parameterList.add(new CommandSingleParameter( + commandParamParts[0].toLowerCase(), + commandParamParts.length > 1 ? + Ts3String.unescape(commandParamParts[1]) : + null + ) + ); + } + } + + return new SimpleCommand(commandLabel, role, parameterList); } } diff --git a/src/main/java/com/github/manevolent/ts3j/command/TypedCommand.java b/src/main/java/com/github/manevolent/ts3j/command/TypedCommand.java deleted file mode 100644 index 83598f8..0000000 --- a/src/main/java/com/github/manevolent/ts3j/command/TypedCommand.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.manevolent.ts3j.command; - -import com.github.manevolent.ts3j.command.part.CommandParameter; -import com.github.manevolent.ts3j.protocol.ProtocolRole; -import com.github.manevolent.ts3j.protocol.client.TeamspeakClient; -import com.github.manevolent.ts3j.protocol.server.TeamspeakServer; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -public abstract class TypedCommand implements Command { - private final ProtocolRole role; - private final String name; - private final Map parameters = new LinkedHashMap<>(); - - protected TypedCommand(ProtocolRole role, String name) { - this.role = role; - - this.name = name; - } - - protected CommandParameter set(CommandParameter commandParameter) { - this.parameters.put(commandParameter.getName(), commandParameter); - - return commandParameter; - } - - public CommandParameter get(String key) { - return this.parameters.get(key); - } - - @Override - public Iterable getParameters() { - return Collections.unmodifiableCollection(parameters.values()); - } - - @Override - public String getName() { - return name; - } - - @Override - public ProtocolRole getRole() { - return role; - } - - @Override - public String toString() { - return build(); - } - - public void handleServer(TeamspeakServer server) { - - } - - public void handleClient(TeamspeakClient client) { - - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java index 00f9ccc..c832b2f 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java +++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java @@ -58,6 +58,11 @@ public class CommandOption implements CommandParameter { throw new UnsupportedOperationException(); } + @Override + public String getValue() { + return enabled ? "true" : "false"; + } + @Override public String toString() { if (enabled) diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java index 3c22f16..a6b9040 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java +++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java @@ -1,5 +1,7 @@ package com.github.manevolent.ts3j.command.part; +import java.util.Base64; + public interface CommandParameter { String getName(); @@ -14,6 +16,12 @@ public interface CommandParameter { void set(float value); void set(double value); + default void set(byte[] value) { + set(Base64.getEncoder().encodeToString(value)); + } + + String getValue(); + /** * Converts the command part to a usable command string element. * @return Command string element. diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java index 907c047..8fbb19f 100644 --- a/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java +++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java @@ -10,6 +10,11 @@ public class CommandSingleParameter implements CommandParameter { this.key = key; } + public CommandSingleParameter(String key, String value) { + this.key = key; + this.value = value; + } + @Override public String toString() { if (value == null) @@ -67,4 +72,9 @@ public class CommandSingleParameter implements CommandParameter { public void set(double value) { this.value = Double.toString(value); } + + @Override + public String getValue() { + return value; + } } diff --git a/src/main/java/com/github/manevolent/ts3j/command/type/client/ClientInitIV.java b/src/main/java/com/github/manevolent/ts3j/command/type/client/ClientInitIV.java deleted file mode 100644 index 73f29ae..0000000 --- a/src/main/java/com/github/manevolent/ts3j/command/type/client/ClientInitIV.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.manevolent.ts3j.command.type.client; - -import com.github.manevolent.ts3j.command.TypedCommand; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; -import com.github.manevolent.ts3j.protocol.ProtocolRole; - -public class ClientInitIV extends TypedCommand { - public ClientInitIV() { - super(ProtocolRole.CLIENT, "clientinitiv"); - - set(new CommandSingleParameter("ot")); - set(new CommandSingleParameter("alpha")); - set(new CommandSingleParameter("omega")); - set(new CommandSingleParameter("ip")); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand.java b/src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand.java deleted file mode 100644 index 2befdf3..0000000 --- a/src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.manevolent.ts3j.command.type.server; - -import com.github.manevolent.ts3j.command.TypedCommand; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; -import com.github.manevolent.ts3j.protocol.ProtocolRole; - -public class ServerInitIVExpand extends TypedCommand { - public ServerInitIVExpand() { - super(ProtocolRole.CLIENT, "initivexpand"); - - set(new CommandSingleParameter("alpha")); - set(new CommandSingleParameter("beta")); - set(new CommandSingleParameter("omega")); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand2.java b/src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand2.java deleted file mode 100644 index 6048fdb..0000000 --- a/src/main/java/com/github/manevolent/ts3j/command/type/server/ServerInitIVExpand2.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.manevolent.ts3j.command.type.server; - -import com.github.manevolent.ts3j.command.TypedCommand; -import com.github.manevolent.ts3j.command.part.CommandSingleParameter; -import com.github.manevolent.ts3j.protocol.ProtocolRole; - -public class ServerInitIVExpand2 extends TypedCommand { - public ServerInitIVExpand2() { - super(ProtocolRole.CLIENT, "initivexpand2"); - - set(new CommandSingleParameter("l")); - set(new CommandSingleParameter("beta")); - set(new CommandSingleParameter("omega")); - set(new CommandSingleParameter("ot")); - set(new CommandSingleParameter("proof")); - set(new CommandSingleParameter("tvd")); - } -} diff --git a/src/main/java/com/github/manevolent/ts3j/identity/Identity.java b/src/main/java/com/github/manevolent/ts3j/identity/Identity.java index 7ee5825..3a36703 100644 --- a/src/main/java/com/github/manevolent/ts3j/identity/Identity.java +++ b/src/main/java/com/github/manevolent/ts3j/identity/Identity.java @@ -1,5 +1,6 @@ package com.github.manevolent.ts3j.identity; +import com.github.manevolent.ts3j.util.Ts3Crypt; import com.github.manevolent.ts3j.util.Ts3Logging; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.DERBitString; @@ -34,7 +35,7 @@ public class Identity { public Identity(ECPoint publicKey) { this.publicKey = publicKey; - this.publicKeyString = Base64.getEncoder().encodeToString(encodePublicKey()); + this.publicKeyString = Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(publicKey)); this.fingerprint = generateFingerprint(); } @@ -136,24 +137,9 @@ public class Identity { return curr; } - private byte[] encodePublicKey() { - try { - byte[] dataArray = new DERSequence(new ASN1Encodable[]{ - new DERBitString(new byte[]{0b0000_0000}, 7), - new DERInteger(32), - new DERInteger(publicKey.getAffineXCoord().toBigInteger()), - new DERInteger(publicKey.getAffineYCoord().toBigInteger()) - }).getEncoded(); - - return dataArray; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - private String generateFingerprint() { synchronized (sha1) { - return Base64.getEncoder().encodeToString(sha1.digest(encodePublicKey())); + return Base64.getEncoder().encodeToString(sha1.digest(Ts3Crypt.encodePublicKey(publicKey))); } } } diff --git a/src/main/java/com/github/manevolent/ts3j/license/EphemeralLicenseUse.java b/src/main/java/com/github/manevolent/ts3j/license/EphemeralLicenseUse.java new file mode 100644 index 0000000..187a9a7 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/license/EphemeralLicenseUse.java @@ -0,0 +1,36 @@ +package com.github.manevolent.ts3j.license; + +import com.github.manevolent.ts3j.enums.LicenseType; + +import java.nio.ByteBuffer; + +public class EphemeralLicenseUse extends LicenseUse { + public EphemeralLicenseUse() { + super(); + } + + @Override + public int getSize() { + return 0; + } + + @Override + public LicenseUseType getUseType() { + return LicenseUseType.EPHEMERAL; + } + + @Override + public LicenseType getLicenseType() { + return LicenseType.UNKNOWN; + } + + @Override + public ByteBuffer write(ByteBuffer buffer) { + return buffer; + } + + @Override + public ByteBuffer read(ByteBuffer buffer) { + return buffer; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/license/IntermediateLicenseUse.java b/src/main/java/com/github/manevolent/ts3j/license/IntermediateLicenseUse.java new file mode 100644 index 0000000..943589f --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/license/IntermediateLicenseUse.java @@ -0,0 +1,65 @@ +package com.github.manevolent.ts3j.license; + +import com.github.manevolent.ts3j.enums.LicenseType; + +import java.nio.ByteBuffer; + +import static com.github.manevolent.ts3j.enums.LicenseType.NO_LICENSE; +import static com.github.manevolent.ts3j.enums.LicenseType.UNKNOWN; + +public class IntermediateLicenseUse extends LicenseUse { + private byte[] unknown = new byte[4]; + private String issuer; + + public IntermediateLicenseUse() { + super(); + } + + @Override + public int getSize() { + return 4 + issuer.length() + 1; + } + + @Override + public LicenseUseType getUseType() { + return LicenseUseType.EPHEMERAL; + } + + @Override + public LicenseType getLicenseType() { + return UNKNOWN; + } + + @Override + public ByteBuffer write(ByteBuffer buffer) { + buffer.put(unknown); + License.writeNullTerminatedLicenseString(buffer, issuer); + + return buffer; + } + + @Override + public ByteBuffer read(ByteBuffer buffer) { + buffer.get(unknown); + + issuer = License.readNullTerminatedLicenseString(buffer); + + return buffer; + } + + public byte[] getUnknown() { + return unknown; + } + + public void setUnknown(byte[] unknown) { + this.unknown = unknown; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/license/License.java b/src/main/java/com/github/manevolent/ts3j/license/License.java new file mode 100644 index 0000000..2254088 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/license/License.java @@ -0,0 +1,182 @@ +package com.github.manevolent.ts3j.license; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +public class License { + public static final byte[] ROOT_KEY = + { + (byte) 0xcd, 0x0d, (byte) 0xe2, (byte) 0xae, (byte) 0xd4, 0x63, 0x45, 0x50, (byte) 0x9a, + 0x7e, 0x3c, (byte) 0xfd, (byte) 0x8f, 0x68, (byte) 0xb3, (byte) 0xdc, 0x75, 0x55, (byte) 0xb2, + (byte) 0x9d, (byte) 0xcc, (byte) 0xec, 0x73, (byte) 0xcd, 0x18, 0x75, 0x0f, (byte) 0x99, + 0x38, 0x12, 0x40, (byte) 0x8a + }; + + private byte[] computedHash; + + private byte[] publicKey = new byte[32]; + private byte licenseBlockType; + private long start, + end; + + private LicenseUse use; + + public LicenseUse getUse() { + return use; + } + + public void setUse(LicenseUse use) { + this.use = use; + this.licenseBlockType = (byte) (use.getUseType().getIndex() & 0xFF); + } + + public byte[] getPublicKey() { + return publicKey; + } + + public void setPublicKey(byte[] publicKey) { + this.publicKey = publicKey; + } + + public byte getLicenseBlockType() { + return licenseBlockType; + } + + public void setLicenseBlockType(byte licenseBlockType) { + this.licenseBlockType = licenseBlockType; + } + + public long getStart() { + return start; + } + + public void setStart(long start) { + this.start = start; + } + + public long getEnd() { + return end; + } + + public void setEnd(long end) { + this.end = end; + } + + public int getSize() { + return 32 + 1 + 8 + use.getSize(); + } + + public ByteBuffer write(ByteBuffer buffer) { + writeHeader(buffer); + writeBody(buffer); + + return buffer; + } + + public ByteBuffer read(ByteBuffer buffer) { + readHeader(buffer); + readBody(buffer); + + return buffer; + } + + + public ByteBuffer readHeader(ByteBuffer buffer) { + byte keyType = buffer.get(); + if (keyType != 0x00) throw new IllegalArgumentException("invalid key type: " + keyType); + + buffer.get(publicKey); + licenseBlockType = buffer.get(); + start = buffer.getInt() & 0x000000FFFFFF; + end = buffer.getInt() & 0x000000FFFFFF; + + return buffer; + } + + public ByteBuffer readBody(ByteBuffer buffer) { + LicenseUseType useType = LicenseUseType.fromId(getLicenseBlockType()); + + try { + use = useType.getUseClass().newInstance(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + use.read(buffer); + + return buffer; + } + + + public ByteBuffer writeHeader(ByteBuffer buffer) { + buffer.put((byte) 0x00); + buffer.put(publicKey); + buffer.put((byte) (licenseBlockType & 0xFF)); + buffer.putInt((int) (start & 0x000000FFFFFF)); + buffer.putInt((int) (end & 0x000000FFFFFF)); + + return buffer; + } + + public ByteBuffer writeBody(ByteBuffer buffer) { + use.write(buffer); + + return buffer; + } + + public static String readNullTerminatedLicenseString(ByteBuffer buffer) { + StringBuilder builder = new StringBuilder(); + while (buffer.remaining() > 0) { + char c = (char) buffer.get(); + switch (c) { + case '\0': + break; + default: + builder.append(c); + } + } + return builder.toString(); + } + + public static void writeNullTerminatedLicenseString(ByteBuffer buffer, String s) { + buffer.put((s + "\0").getBytes(Charset.forName("ASCII"))); + } + + public static List readLicenses(ByteBuffer buffer) { + List licenses = new ArrayList<>(); + + while (buffer.remaining() > 0) { + byte licenseVersion = buffer.get(); + if (licenseVersion != 0x01) + throw new IllegalArgumentException("invalid license version"); + + License license = new License(); + license.read(buffer); + licenses.add(license); + } + + return licenses; + } + + public byte[] getHash() { + if (computedHash == null) { + ByteBuffer buffer = ByteBuffer.allocate(getSize()); + write(buffer); + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA512"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + return digest.digest(buffer.array()); + } + + return computedHash; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/license/LicenseUse.java b/src/main/java/com/github/manevolent/ts3j/license/LicenseUse.java new file mode 100644 index 0000000..443ee19 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/license/LicenseUse.java @@ -0,0 +1,20 @@ +package com.github.manevolent.ts3j.license; + +import com.github.manevolent.ts3j.enums.LicenseType; + +import java.nio.ByteBuffer; + +public abstract class LicenseUse { + protected LicenseUse() { + } + + public abstract int getSize(); + + public abstract LicenseUseType getUseType(); + + public abstract LicenseType getLicenseType(); + + public abstract ByteBuffer write(ByteBuffer buffer); + + public abstract ByteBuffer read(ByteBuffer buffer); +} diff --git a/src/main/java/com/github/manevolent/ts3j/license/LicenseUseType.java b/src/main/java/com/github/manevolent/ts3j/license/LicenseUseType.java new file mode 100644 index 0000000..a77971f --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/license/LicenseUseType.java @@ -0,0 +1,31 @@ +package com.github.manevolent.ts3j.license; + +public enum LicenseUseType { + INTERMEDIATE(0, IntermediateLicenseUse.class), + SERVER(2, ServerLicenseUse.class), + EPHEMERAL(32, EphemeralLicenseUse.class) + ; + + private final int index; + private final Class use; + + LicenseUseType(int index, Class use) { + this.index = index; + this.use = use; + } + + public int getIndex() { + return index; + } + + public static LicenseUseType fromId(int index) { + for (LicenseUseType value : values()) + if (value.getIndex() == index) return value; + + throw new IllegalArgumentException("invalid index: " + index); + } + + public Class getUseClass() { + return use; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/license/ServerLicenseUse.java b/src/main/java/com/github/manevolent/ts3j/license/ServerLicenseUse.java new file mode 100644 index 0000000..4ecb019 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/license/ServerLicenseUse.java @@ -0,0 +1,71 @@ +package com.github.manevolent.ts3j.license; + +import com.github.manevolent.ts3j.enums.LicenseType; + +import java.nio.ByteBuffer; + +public class ServerLicenseUse extends LicenseUse { + private byte type; + private byte[] unknown = new byte[4]; + private String issuer; + + public ServerLicenseUse() { + super(); + } + + @Override + public int getSize() { + return 1 + 4 + issuer.length() + 1; + } + + @Override + public LicenseUseType getUseType() { + return LicenseUseType.SERVER; + } + + @Override + public ByteBuffer write(ByteBuffer buffer) { + buffer.put(type); + buffer.put(unknown); + License.writeNullTerminatedLicenseString(buffer, issuer); + + return buffer; + } + + @Override + public ByteBuffer read(ByteBuffer buffer) { + type = buffer.get(); + buffer.get(unknown); + issuer = License.readNullTerminatedLicenseString(buffer); + + return buffer; + } + + public byte[] getUnknown() { + return unknown; + } + + public void setUnknown(byte[] unknown) { + this.unknown = unknown; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public LicenseType getLicenseType() { + return LicenseType.fromId(type); + } + + public void setType(byte type) { + this.type = type; + } + + public byte getType() { + return type; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java index f05502c..2d85ac3 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/Packet2Command.java @@ -1,5 +1,6 @@ package com.github.manevolent.ts3j.protocol.packet; +import com.github.manevolent.ts3j.command.SimpleCommand; import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.util.Ts3Logging; @@ -17,6 +18,10 @@ public class Packet2Command extends Packet { return text; } + public SimpleCommand parse() { + return SimpleCommand.parse(getRole(), getText()); + } + public void setText(String text) { this.text = text; } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java index 0d4c75b..48e93a2 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java @@ -1,17 +1,31 @@ package com.github.manevolent.ts3j.protocol.packet.handler.client; -import com.github.manevolent.ts3j.command.type.client.ClientInitIV; +import com.github.manevolent.ts3j.command.SimpleCommand; +import com.github.manevolent.ts3j.command.part.CommandSingleParameter; +import com.github.manevolent.ts3j.license.License; import com.github.manevolent.ts3j.protocol.NetworkPacket; import com.github.manevolent.ts3j.protocol.ProtocolRole; import com.github.manevolent.ts3j.protocol.client.LocalTeamspeakClient; import com.github.manevolent.ts3j.protocol.packet.Packet2Command; import com.github.manevolent.ts3j.protocol.packet.Packet8Init1; +import com.github.manevolent.ts3j.util.Ts3Crypt; import com.github.manevolent.ts3j.util.Ts3Logging; +import net.i2p.crypto.eddsa.math.GroupElement; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.DSADigestSigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +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.List; import java.util.Random; public class LocalClientHandlerConnecting extends LocalClientHandler { @@ -118,14 +132,22 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { alphaBytes = new byte[10]; new Random().nextBytes(alphaBytes); - ClientInitIV command = new ClientInitIV(); + SimpleCommand initiv = new SimpleCommand("clientinitiv", ProtocolRole.CLIENT); - command.get("alpha").set(Base64.getEncoder().encodeToString(alphaBytes)); - command.get("omega").set(getClient().getLocalIdentity().getPublicKeyString()); - command.get("ot").set(1); // ? - command.get("ip").set(null); + initiv.add(new CommandSingleParameter("alpha", Base64.getEncoder().encodeToString(alphaBytes))); - step4.setClientIVcommand(command.build().getBytes(Charset.forName("UTF8"))); + initiv.add(new CommandSingleParameter("omega", getClient().getLocalIdentity().getPublicKeyString())); + + initiv.add(new CommandSingleParameter("ot", "1")); // constant, set to 1 + + initiv.add( + new CommandSingleParameter( + "ip", + getClient().getOption("client.hostname", String.class) + ) + ); + + step4.setClientIVcommand(initiv.build().getBytes(Charset.forName("UTF8"))); response4.setStep(step4); @@ -136,7 +158,38 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { } } else if (packet.getPacket() instanceof Packet2Command) { - Ts3Logging.debug(((Packet2Command) packet.getPacket()).getText()); + SimpleCommand command = ((Packet2Command) packet.getPacket()).parse(); + + if (command.getName().equalsIgnoreCase("initivexpand2")) { + // 3.2.2 initivexpand2 (Client <- Server) + + Ts3Logging.debug("initivexpand2"); + + if (!command.get("ot").getValue().equals("1")) + throw new IllegalArgumentException("ot constant != 1: " + command.get("ot").getValue()); + + byte[] license = Base64.getDecoder().decode(command.get("l").getValue()); + byte[] licsense_validation = command.get("tvd").getValue() == null ? + null : + Base64.getDecoder().decode(command.get("tvd").getValue()); + + byte[] random = Base64.getDecoder().decode(command.get("beta").getValue()); // beta + byte[] publicKeyBytes = Base64.getDecoder().decode(command.get("omega").getValue()); // omega + byte[] proof = Base64.getDecoder().decode(command.get("proof").getValue()); // ecdh_sign(1) + + // 3.2.1.1 Verify integrity + // The proof parameter is the sign of the l parameter (not base64 encoded). The client can verify the l + // parameter with the public key of the server which is sent in omega. + + ECPoint publicKey = Ts3Crypt.decodePublicKey(publicKeyBytes); + + if (!Ts3Crypt.verifySignature(publicKey, license, proof)) + throw new SecurityException("invalid proof signature: " + Ts3Logging.getHex(proof)); + + // 3.2.2.2 Parsing the license + List licenses = License.readLicenses(ByteBuffer.wrap(license)); + + } } } } diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java index 5875eb2..1c459c8 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/LocalTeamspeakSocket.java @@ -60,10 +60,11 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea packet.setSocketAddress(datagramSocket.getRemoteSocketAddress()); Ts3Logging.debug( - "[NETWORK] WRITE Len=" + packet.getLength() + " to " + packet.getSocketAddress() + - "\n" + Ts3Logging.getHex(packet.getData(), packet.getLength()) + "[NETWORK] WRITE Len=" + packet.getLength() + " to " + packet.getSocketAddress() ); + //Ts3Logging.debug(Ts3Logging.getHex(packet.getData(), packet.getLength())); + datagramSocket.send(packet); } @@ -116,10 +117,11 @@ public class LocalTeamspeakSocket extends AbstractTeamspeakSocket implements Tea datagramSocket.receive(datagramPacket); Ts3Logging.debug( - "[NETWORK] READ Len=" + datagramPacket.getLength() + " from " + datagramPacket.getSocketAddress() + - "\n" + Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength()) + "[NETWORK] READ Len=" + datagramPacket.getLength() + " from " + datagramPacket.getSocketAddress() ); + //Ts3Logging.debug(Ts3Logging.getHex(datagramPacket.getData(), datagramPacket.getLength())); + return datagramPacket; } diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java new file mode 100644 index 0000000..d82fdac --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java @@ -0,0 +1,82 @@ +package com.github.manevolent.ts3j.util; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyGenerationParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.DSADigestSigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.math.ec.ECPoint; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +public final class Ts3Crypt { + private static ECDomainParameters getDomainParameters() { + ECNamedCurveParameterSpec ecp = ECNamedCurveTable.getParameterSpec("prime256v1"); + return new ECDomainParameters(ecp.getCurve(), ecp.getG(), ecp.getN(), ecp.getH(), ecp.getSeed()); + } + + public static boolean verifySignature(ECPoint publicKey, byte[] data, byte[] signature) { + DSADigestSigner signer = new DSADigestSigner(new ECDSASigner(), new SHA256Digest()); + ECPublicKeyParameters signingKey = new ECPublicKeyParameters(publicKey, getDomainParameters()); + + signer.init(false, signingKey); + signer.update(data, 0, data.length); + + return signer.verifySignature(signature); + } + + /** + * From http://stackoverflow.com/questions/2409618/how-do-i-decode-a-der-encoded-string-in-java + */ + private static ASN1Encodable[] readDERObject(byte[] data) throws IOException + { + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + ASN1InputStream asnInputStream = new ASN1InputStream(inStream); + + ASN1Object object = asnInputStream.readObject(); + + if (object instanceof DLSequence) { + return ((DLSequence) object).toArray(); + } else { + return new ASN1Encodable[] { object }; + } + } + + public static ECPoint decodePublicKey(byte[] bytes) { + try { + DERSequence asnKeyData = new DERSequence(readDERObject(bytes)); + + BigInteger x = ((org.bouncycastle.asn1.ASN1Integer)asnKeyData.getObjectAt(2)).getValue(); + BigInteger y = ((org.bouncycastle.asn1.ASN1Integer)asnKeyData.getObjectAt(3)).getValue(); + + return getDomainParameters().getCurve().createPoint(x, y); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static byte[] encodePublicKey(ECPoint publicKey) { + try { + byte[] dataArray = new DERSequence(new ASN1Encodable[]{ + new DERBitString(new byte[]{0b0000_0000}, 7), + new DERInteger(32), + new DERInteger(publicKey.getAffineXCoord().toBigInteger()), + new DERInteger(publicKey.getAffineYCoord().toBigInteger()) + }).getEncoded(); + + return dataArray; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java index aeafc1d..7ab7b32 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java @@ -43,7 +43,7 @@ public final class Ts3String { default: throw new IllegalArgumentException("invalid escape code: " + c); } - } + } else sb.append(c); } return sb.toString(); diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java index 0b094f2..7b448d9 100644 --- a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java +++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java @@ -21,7 +21,7 @@ public class ServerConnectionTest extends TestCase { client.setLocalIdentity(localIdentity); client.connect(new InetSocketAddress( - "voice.teamspeak.com", + "ts.teamlixo.net", 9987), null, 10000L