From 72c585a089d0279207999e6095f30b04e829b2dc Mon Sep 17 00:00:00 2001 From: Manevolent Date: Fri, 21 Dec 2018 09:57:49 -0500 Subject: [PATCH] Ts3 INI saving, API is in. Loads into the official client, too. Check out LocalIdentity.save() methods --- .../manevolent/ts3j/identity/Identity.java | 16 +++ .../ts3j/identity/LocalIdentity.java | 131 +++++++++++++++++- .../github/manevolent/ts3j/util/Ts3Crypt.java | 1 - .../manevolent/ts3j/IdentitySaveTest.java | 56 ++++++++ 4 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/github/manevolent/ts3j/IdentitySaveTest.java 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 d14913a..2945b8a 100644 --- a/src/main/java/com/github/manevolent/ts3j/identity/Identity.java +++ b/src/main/java/com/github/manevolent/ts3j/identity/Identity.java @@ -2,8 +2,13 @@ package com.github.manevolent.ts3j.identity; import com.github.manevolent.ts3j.util.Ts3Crypt; import com.github.manevolent.ts3j.util.Ts3Debugging; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.math.ec.ECPoint; +import java.io.IOException; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -32,6 +37,17 @@ public class Identity { this.uid = generateUid(); } + public byte[] toASN() throws IOException { + return new DERSequence( + new ASN1Encodable[] { + new DERBitString(0), + new ASN1Integer(32), + new ASN1Integer(getPublicKey().getXCoord().toBigInteger()), + new ASN1Integer(getPublicKey().getYCoord().toBigInteger()) + } + ).getEncoded(); + } + public ECPoint getPublicKey() { return publicKey; } diff --git a/src/main/java/com/github/manevolent/ts3j/identity/LocalIdentity.java b/src/main/java/com/github/manevolent/ts3j/identity/LocalIdentity.java index 0268918..0fc1258 100644 --- a/src/main/java/com/github/manevolent/ts3j/identity/LocalIdentity.java +++ b/src/main/java/com/github/manevolent/ts3j/identity/LocalIdentity.java @@ -2,8 +2,7 @@ package com.github.manevolent.ts3j.identity; import com.github.manevolent.ts3j.util.Ts3Crypt; import com.github.manevolent.ts3j.util.Ts3Debugging; -import org.bouncycastle.asn1.ASN1Object; -import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.*; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.bouncycastle.crypto.params.ECDomainParameters; @@ -20,6 +19,8 @@ import java.math.BigInteger; import java.nio.charset.Charset; import java.security.*; import java.util.Base64; +import java.util.HashMap; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -104,6 +105,132 @@ public class LocalIdentity extends Identity { return privateKey; } + @Override + public byte[] toASN() throws IOException { + return new DERSequence( + new ASN1Encodable[] { + new DERBitString(128), + new ASN1Integer(32), + new ASN1Integer(getPublicKey().getXCoord().toBigInteger()), + new ASN1Integer(getPublicKey().getYCoord().toBigInteger()), + new ASN1Integer(getPrivateKey()) + } + ).getEncoded(); + } + + /** + * Saves this local identity to a file + * @param file File to save to + * @throws IOException + */ + public void save(File file) throws IOException { + save(file, null); + } + + /** + * Saves this local identity to a file + * @param file File to save to + * @param properties Additional properties to supply in the INI + * @throws IOException + */ + public void save(File file, Map properties) throws IOException { + save(new FileOutputStream(file), properties); + } + + /** + * Saves this local identity to an output stream (i.e.,file) + * @param outputStream OutputStream instance to save raw bytes to + * @throws IOException + */ + public void save(OutputStream outputStream) throws IOException { + save(outputStream, null); + } + + /** + * Saves this local identity to an output stream (i.e.,file) + * @param outputStream OutputStream instance to save raw bytes to + * @param properties Additional properties to supply in the INI + * @throws IOException + */ + public void save(OutputStream outputStream, Map properties) throws IOException { + try (Writer writer = new OutputStreamWriter(outputStream)) { + writer.write(export(properties)); + } + } + + /** + * Exports a local identity object to a Teamspeak3 client friendly format. + * @return TS3 identity format + */ + public String export() throws IOException { + return export(null); + } + + /** + * Exports a local identity object to a Teamspeak3 client friendly format. + * @param properties Additional properties to supply in the INI + * @return TS3 identity format + */ + public String export(Map properties) throws IOException { + Map iniFile = new HashMap<>(); + + if (properties != null) { + // id, nickname, phonetic_nickname, etc. + for (Map.Entry entry : properties.entrySet()) { + iniFile.put(entry.getKey(), entry.getValue()); + } + } + + StringBuilder identityDataBuilder = new StringBuilder(); + + // Encode key offset + identityDataBuilder.append(getKeyOffset()).append("V"); + + // Generate data for TS3 identity + byte[] identityData = toASN(); + + // Encode to UTF8 + identityData = Base64.getEncoder().encodeToString(identityData).getBytes("UTF8"); + + // Encrypt identity + Ts3Crypt.xor( + identityData, 0, + identityFileObfuscationKey, 0, + Math.min(100, identityData.length), + identityData, 0 + ); + + int nullIndex = 0x0; // STATIC + + byte[] hash = Ts3Crypt.hash128( + identityData, + 20, + nullIndex < 0 ? identityData.length - 20 : nullIndex + ); + + Ts3Crypt.xor( + identityData, 0, + hash, 0, + 20, + identityData, 0 + ); + + // Place down Base64 identity data for TS3 identity + identityDataBuilder.append(Base64.getEncoder().encodeToString(identityData)); + + iniFile.put("identity", identityDataBuilder.toString()); + + StringBuilder builder = new StringBuilder(); + builder.append("[Identity]").append("\r\n"); + for (Map.Entry entry : iniFile.entrySet()) { + builder.append(entry.getKey()) + .append("=") + .append("\"").append(entry.getValue().replace("\"", "\\\"")).append("\"") + .append("\r\n"); + } + return builder.toString(); + } + /** * Reads a TS3 identity from a given TS3 identity INI file. * @param iniFile INI file to read diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java index 89cc01f..f4e0f9a 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java @@ -231,7 +231,6 @@ public final class Ts3Crypt { { ByteArrayInputStream inStream = new ByteArrayInputStream(data); ASN1InputStream asnInputStream = new ASN1InputStream(inStream); - ASN1Object object = asnInputStream.readObject(); if (object instanceof DLSequence) { diff --git a/src/test/java/com/github/manevolent/ts3j/IdentitySaveTest.java b/src/test/java/com/github/manevolent/ts3j/IdentitySaveTest.java new file mode 100644 index 0000000..4e3a41c --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/IdentitySaveTest.java @@ -0,0 +1,56 @@ +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 java.io.File; +import java.io.StringBufferInputStream; +import java.util.Base64; + +public class IdentitySaveTest extends TestCase { + public static void main(String[] args) throws Exception { + new IdentitySaveTest().testParser(); + } + + public void testParser() throws Exception { + Ts3Debugging.setEnabled(true); + + String testIni = "[Identity]\n" + + "id=New identity_1\n" + + "identity=\"171V9UfzzH5tGS4W" + + "V8zptzxuvrqBAWUAMVNkUQJZUgl3XXYJQnBQY0" + + "VnJAsDIgR/VgFiB0ZFBQBhcRFRQn0Jc3ZgVGZH" + + "e0B9IlBiCjRQKw44OSJGX3t3VGZ2K3U3Xl9BLl" + + "95KmB3S3l5QUdBaUJDWG1VWi9jQUtwQUUxbFcr" + + "cnhNdGlaR1ZJUGIzZEtsSWx3cUVMKzVoSzVRPT" + + "0=\"\n"; + + LocalIdentity localIdentity = LocalIdentity.read(new StringBufferInputStream(testIni)); + + // Assert that the output of toASN() is fixed, and matches what we expect in the above identity output + // this does not certify encryption; this only affixes the machine-readable ASN1 format + assertEquals( + "MG4DAgeAAgEgAiAOvndZ5WbTh68cjFhCl" + + "uDgPwPFh1DaJ24Sdst70SBw4wIhANPfVvKpIGi" + + "RkPaOoYXGwoLB0SEIBSkhpMlIOPAxyyAGAiBCX" + + "mUZ/cAKpAE1lW+rxMtiZGVIPb3dKlIlwqEL+5h" + + "K5Q==", + Base64.getEncoder().encodeToString(localIdentity.toASN()) + ); + + String identityData = localIdentity.export(null); + + LocalIdentity localIdentity2 = LocalIdentity.read(new StringBufferInputStream(identityData)); + + assertEquals(localIdentity.getSecurityLevel(), localIdentity2.getSecurityLevel()); + assertEquals(localIdentity.getPublicKeyString(), localIdentity2.getPublicKeyString()); + + // If you decide to save this AND import in Teamspeak3, you need to supply a HashMap with the following: + // phonetic_nickname + // nickname + // id + //localIdentity.save(new File("out.ini")); + } +}