Ts3 INI identity loading
This commit is contained in:
@@ -51,5 +51,11 @@
|
|||||||
<artifactId>dnsjava</artifactId>
|
<artifactId>dnsjava</artifactId>
|
||||||
<version>2.1.8</version>
|
<version>2.1.8</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ini4j</groupId>
|
||||||
|
<artifactId>ini4j</artifactId>
|
||||||
|
<version>0.5.1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.github.manevolent.ts3j.identity;
|
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 org.bouncycastle.asn1.ASN1Object;
|
||||||
|
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;
|
||||||
@@ -10,11 +13,32 @@ import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
|||||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||||
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
|
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
|
||||||
import org.bouncycastle.math.ec.ECPoint;
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
|
import org.ini4j.Ini;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class LocalIdentity extends Identity {
|
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;
|
private final BigInteger privateKey;
|
||||||
|
|
||||||
public LocalIdentity(ECPoint publicKey, BigInteger privateKey) {
|
public LocalIdentity(ECPoint publicKey, BigInteger privateKey) {
|
||||||
@@ -23,15 +47,32 @@ public class LocalIdentity extends Identity {
|
|||||||
this.privateKey = privateKey;
|
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,
|
public static LocalIdentity load(ECPoint publicKey,
|
||||||
BigInteger privateKey) {
|
BigInteger privateKey) {
|
||||||
return new LocalIdentity(publicKey, 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) {
|
public static LocalIdentity load(BigInteger privateKey) {
|
||||||
return new LocalIdentity(Ts3Crypt.generatePublicKeyFromPrivateKey(privateKey), 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 {
|
public static LocalIdentity generateNew(int securityLevel) throws GeneralSecurityException {
|
||||||
ECNamedCurveParameterSpec ecp = ECNamedCurveTable.getParameterSpec("prime256v1");
|
ECNamedCurveParameterSpec ecp = ECNamedCurveTable.getParameterSpec("prime256v1");
|
||||||
ECDomainParameters domainParams =
|
ECDomainParameters domainParams =
|
||||||
@@ -55,7 +96,85 @@ public class LocalIdentity extends Identity {
|
|||||||
return Ts3Crypt.createSignature(privateKey, data);
|
return Ts3Crypt.createSignature(privateKey, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the identity's private component.
|
||||||
|
* @return private key.
|
||||||
|
*/
|
||||||
public BigInteger getPrivateKey() {
|
public BigInteger getPrivateKey() {
|
||||||
return privateKey;
|
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<byte> a, ReadOnlySpan<byte> b, int len, Span<byte> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Punisher.NaCl.Internal.Sha512;
|
|||||||
import com.github.manevolent.ts3j.identity.*;
|
import com.github.manevolent.ts3j.identity.*;
|
||||||
import com.github.manevolent.ts3j.license.License;
|
import com.github.manevolent.ts3j.license.License;
|
||||||
import org.bouncycastle.asn1.*;
|
import org.bouncycastle.asn1.*;
|
||||||
|
import org.bouncycastle.crypto.KeyGenerationParameters;
|
||||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||||
@@ -124,7 +125,7 @@ public final class Ts3Crypt {
|
|||||||
return parameters;
|
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)
|
if (a.length < len || b.length < len || outBuf.length < len)
|
||||||
throw new ArrayIndexOutOfBoundsException();
|
throw new ArrayIndexOutOfBoundsException();
|
||||||
@@ -290,6 +291,28 @@ public final class Ts3Crypt {
|
|||||||
return ECNamedCurveTable.getParameterSpec("prime256v1").getG().multiply(privateKey).normalize();
|
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 {
|
public static final class SecureChannelParameters {
|
||||||
private final byte[] fakeSignature;
|
private final byte[] fakeSignature;
|
||||||
private final byte[] ivStruct;
|
private final byte[] ivStruct;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user