From 91187964c5a0933c0c3df6612b62dac44692c77f Mon Sep 17 00:00:00 2001 From: Manevolent Date: Mon, 5 Nov 2018 14:48:31 -0500 Subject: [PATCH] Ts3 INI identity loading --- pom.xml | 6 + .../ts3j/identity/LocalIdentity.java | 119 ++++++++++++++++++ .../github/manevolent/ts3j/util/Ts3Crypt.java | 25 +++- .../manevolent/ts3j/IdentityFileTest.java | 38 ++++++ 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/manevolent/ts3j/IdentityFileTest.java diff --git a/pom.xml b/pom.xml index faabc8b..1b226a0 100644 --- a/pom.xml +++ b/pom.xml @@ -51,5 +51,11 @@ dnsjava 2.1.8 + + + org.ini4j + ini4j + 0.5.1 + \ No newline at end of file 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 1ca481b..0268918 100644 --- a/src/main/java/com/github/manevolent/ts3j/identity/LocalIdentity.java +++ b/src/main/java/com/github/manevolent/ts3j/identity/LocalIdentity.java @@ -1,6 +1,9 @@ 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.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.bouncycastle.crypto.params.ECDomainParameters; @@ -10,11 +13,32 @@ import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.math.ec.ECPoint; +import org.ini4j.Ini; +import java.io.*; import java.math.BigInteger; +import java.nio.charset.Charset; import java.security.*; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class LocalIdentity extends Identity { + /** + * Pattern used for Identity.identity value in TS3 identity INI files. + */ + private static final Pattern identityPattern = Pattern.compile("^\"(\\d+)V([\\w\\/\\+]+={0,2})\"$", Pattern.MULTILINE); + + /** + * Key used to obfuscate the key component(s) in official TS3 identity INI files. + */ + private static final byte[] identityFileObfuscationKey = + ("b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d5" + + "1c512023fe54a280201004e90ad1daaae1075d53" + + "b7d571c30e063b5a62a4a017bb394833aa0983e6e").getBytes(Charset.forName("ASCII")); + /** + * Identity private component. + */ private final BigInteger privateKey; public LocalIdentity(ECPoint publicKey, BigInteger privateKey) { @@ -23,15 +47,32 @@ public class LocalIdentity extends Identity { this.privateKey = privateKey; } + /** + * Loads an identity from a specific key-pair. + * @param publicKey public key component + * @param privateKey private key component + * @return local identity with given components + */ public static LocalIdentity load(ECPoint publicKey, BigInteger privateKey) { return new LocalIdentity(publicKey, privateKey); } + /** + * Loads an identity, re-generating the public component from the private component. + * @param privateKey private key component + * @return local identity with given components + */ public static LocalIdentity load(BigInteger privateKey) { return new LocalIdentity(Ts3Crypt.generatePublicKeyFromPrivateKey(privateKey), privateKey); } + /** + * Generates a new identity with a given security level target. + * @param securityLevel security level to generate for (may take time) + * @return local identity with given security level + * @throws GeneralSecurityException + */ public static LocalIdentity generateNew(int securityLevel) throws GeneralSecurityException { ECNamedCurveParameterSpec ecp = ECNamedCurveTable.getParameterSpec("prime256v1"); ECDomainParameters domainParams = @@ -55,7 +96,85 @@ public class LocalIdentity extends Identity { return Ts3Crypt.createSignature(privateKey, data); } + /** + * Gets the identity's private component. + * @return private key. + */ public BigInteger getPrivateKey() { return privateKey; } + + /** + * Reads a TS3 identity from a given TS3 identity INI file. + * @param iniFile INI file to read + * @return local identity + */ + public static final LocalIdentity read(File iniFile) throws IOException { + return read(new FileInputStream(iniFile)); + } + + /** + * Reads a TS3 identity from a given input stream. + * @param inputStream Input stream to read + * @return local identity + */ + public static final LocalIdentity read(InputStream inputStream) throws IOException { + Ini ini = new Ini(inputStream); + + String identityValue = ini.get("Identity", "identity"); + if (identityValue == null) + throw new IOException(new IllegalArgumentException("missing identity value")); + + Matcher matcher = identityPattern.matcher(identityValue); + if (!matcher.matches()) + throw new IOException(new IllegalArgumentException("invalid INI file")); + + long level = Long.parseUnsignedLong(matcher.group(1)); + + //var ident = Base64Decode(match.Groups["identity"].Value); + byte[] identityData = Base64.getDecoder().decode(matcher.group(2)); + + if (identityData.length < 20) throw new IOException(new IllegalArgumentException("identity data too short")); + + //int nullIdx = identityArr.AsSpan(20).IndexOf((byte)0); + int nullIndex = -1; + for (int i = 20; i < identityData.length; i ++) + if (identityData[i] == 0x0) { + nullIndex = i-20; + break; + } + + //var hash = Hash1It(identityArr, 20, nullIdx < 0 ? identityArr.Length - 20 : nullIdx); + byte[] hash = Ts3Crypt.hash128( + identityData, + 20, + nullIndex < 0 ? identityData.length - 20 : nullIndex + ); + + //XorBinary(ReadOnlySpan a, ReadOnlySpan b, int len, Span outBuf) + //XorBinary(identityArr, hash, 20, identityArr); + Ts3Crypt.xor( + identityData, 0, + hash, 0, + 20, + identityData, 0 + ); + + //XorBinary(identityArr, Ts3IdentityObfuscationKey, Math.Min(100, identityArr.Length), identityArr); + Ts3Crypt.xor( + identityData, 0, + identityFileObfuscationKey, 0, + Math.min(100, identityData.length), + identityData, 0 + ); + + String utf8Decoded = new String(identityData, Charset.forName("UTF8")); + + LocalIdentity identity = Ts3Crypt.loadIdentityFromAsn(Base64.getDecoder().decode(utf8Decoded)); + + identity.setKeyOffset(level); + identity.setLastCheckedKeyOffset(level); + + return identity; + } } 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 ffeb1ac..89cc01f 100644 --- a/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java +++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Crypt.java @@ -8,6 +8,7 @@ import Punisher.NaCl.Internal.Sha512; import com.github.manevolent.ts3j.identity.*; import com.github.manevolent.ts3j.license.License; import org.bouncycastle.asn1.*; +import org.bouncycastle.crypto.KeyGenerationParameters; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; @@ -124,7 +125,7 @@ public final class Ts3Crypt { return parameters; } - private static void xor(byte[] a, int aoffs, byte[] b, int boffs, int len, byte[] outBuf, int outOffs) + public static void xor(byte[] a, int aoffs, byte[] b, int boffs, int len, byte[] outBuf, int outOffs) { if (a.length < len || b.length < len || outBuf.length < len) throw new ArrayIndexOutOfBoundsException(); @@ -290,6 +291,28 @@ public final class Ts3Crypt { return ECNamedCurveTable.getParameterSpec("prime256v1").getG().multiply(privateKey).normalize(); } + public static LocalIdentity loadIdentityFromAsn(byte[] asnData) throws IOException { + ECPoint publicKey; + BigInteger privateKey = null; + + DERSequence asnKeyData = new DERSequence(readDERObject(asnData)); + int bitInfo = ((DERBitString)asnKeyData.getObjectAt(0)).intValue(); + if (bitInfo == 0 || bitInfo == 128) { + BigInteger x = ((ASN1Integer)asnKeyData.getObjectAt(2)).getValue(); + BigInteger y = ((ASN1Integer)asnKeyData.getObjectAt(3)).getValue(); + + publicKey = getDomainParameters().getCurve().createPoint(x, y); + + if (bitInfo == 128) + privateKey = ((ASN1Integer)asnKeyData.getObjectAt(4)).getValue(); + } else if (bitInfo == 192) { + privateKey = ((ASN1Integer)asnKeyData.getObjectAt(2)).getValue(); + publicKey = generatePublicKeyFromPrivateKey(privateKey); + } else throw new IllegalArgumentException("invalid DER sequence"); + + return new LocalIdentity(publicKey, privateKey); + } + public static final class SecureChannelParameters { private final byte[] fakeSignature; private final byte[] ivStruct; diff --git a/src/test/java/com/github/manevolent/ts3j/IdentityFileTest.java b/src/test/java/com/github/manevolent/ts3j/IdentityFileTest.java new file mode 100644 index 0000000..0beefa2 --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/IdentityFileTest.java @@ -0,0 +1,38 @@ +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.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.Charset; + +public class IdentityFileTest extends TestCase { + public static void main(String[] args) throws Exception { + new IdentityFileTest().testParser(); + } + + public void testParser() throws Exception { + Ts3Debugging.setEnabled(true); + + String identityFileContents = "[Identity]\n" + + "id=New identity_1\n" + + "identity=\"68616VFe2bVjeiXc+7ULXTnBkEdLZOpjIGP15CFmYbBix+T1EqQEd+WXAPNExRKA8MLl" + + "F/MmxzWwJ3BDwrT1sie31RRwdVSUd5XVVJCTZ+EQIKGAkeZF8HUU9/VwUoWl1mKAJRHGB+QHpXMEFBa" + + "UVBa0lBTkU2VFpDcHE1K2cvamVjbTR0K1FhNmI3VVdHR2o3Nys4ZTFQYm14Zz0=\"\n" + + "nickname=TS3JTest\n" + + "phonetic_nickname=\n"; + + String uuid = "jJbyG5OzsAkEVGsRawC4VhLfeW4="; + int securityLevel = 15; + + LocalIdentity localIdentity = + LocalIdentity.read(new ByteArrayInputStream(identityFileContents.getBytes(Charset.forName("UTF8")))); + + assertEquals(uuid, localIdentity.getUid().toBase64()); + assertEquals(securityLevel, localIdentity.getSecurityLevel()); + } +}