Ts3 INI saving, API is in. Loads into the official client, too. Check out LocalIdentity.save() methods
This commit is contained in:
@@ -2,8 +2,13 @@ package com.github.manevolent.ts3j.identity;
|
|||||||
|
|
||||||
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
||||||
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
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 org.bouncycastle.math.ec.ECPoint;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -32,6 +37,17 @@ public class Identity {
|
|||||||
this.uid = generateUid();
|
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() {
|
public ECPoint getPublicKey() {
|
||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ package com.github.manevolent.ts3j.identity;
|
|||||||
|
|
||||||
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
||||||
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||||
import org.bouncycastle.asn1.ASN1Object;
|
import org.bouncycastle.asn1.*;
|
||||||
import org.bouncycastle.asn1.DERSequence;
|
|
||||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||||
@@ -20,6 +19,8 @@ import java.math.BigInteger;
|
|||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -104,6 +105,132 @@ public class LocalIdentity extends Identity {
|
|||||||
return privateKey;
|
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<String, String> 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<String, String> 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<String, String> properties) throws IOException {
|
||||||
|
Map<String, String> iniFile = new HashMap<>();
|
||||||
|
|
||||||
|
if (properties != null) {
|
||||||
|
// id, nickname, phonetic_nickname, etc.
|
||||||
|
for (Map.Entry<String, String> 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<String, String> 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.
|
* Reads a TS3 identity from a given TS3 identity INI file.
|
||||||
* @param iniFile INI file to read
|
* @param iniFile INI file to read
|
||||||
|
|||||||
@@ -231,7 +231,6 @@ public final class Ts3Crypt {
|
|||||||
{
|
{
|
||||||
ByteArrayInputStream inStream = new ByteArrayInputStream(data);
|
ByteArrayInputStream inStream = new ByteArrayInputStream(data);
|
||||||
ASN1InputStream asnInputStream = new ASN1InputStream(inStream);
|
ASN1InputStream asnInputStream = new ASN1InputStream(inStream);
|
||||||
|
|
||||||
ASN1Object object = asnInputStream.readObject();
|
ASN1Object object = asnInputStream.readObject();
|
||||||
|
|
||||||
if (object instanceof DLSequence) {
|
if (object instanceof DLSequence) {
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user