diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBodyCompressed.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBodyCompressed.java new file mode 100644 index 0000000..8aa1e32 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/PacketBodyCompressed.java @@ -0,0 +1,43 @@ +package com.github.manevolent.ts3j.protocol.packet; + +import com.github.manevolent.ts3j.protocol.ProtocolRole; + +import java.nio.ByteBuffer; + +public class PacketBodyCompressed extends PacketBody { + private byte[] compressed; + + public PacketBodyCompressed(PacketBodyType type, ProtocolRole role) { + super(type, role); + } + + public PacketBodyCompressed(PacketBodyType type, ProtocolRole role, byte[] body) { + super(type, role); + + this.compressed = body; + } + + public byte[] getCompressed() { + return compressed; + } + + public void setCompressed(byte[] compressed) { + this.compressed = compressed; + } + + @Override + public void read(ByteBuffer buffer) { + compressed = new byte[buffer.remaining()]; + buffer.get(compressed); + } + + @Override + public void write(ByteBuffer buffer) { + buffer.put(compressed); + } + + @Override + public int getSize() { + return compressed.length; + } +} diff --git a/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java b/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java new file mode 100644 index 0000000..f5d0c03 --- /dev/null +++ b/src/main/java/com/github/manevolent/ts3j/util/QuickLZ.java @@ -0,0 +1,486 @@ +package com.github.manevolent.ts3j.util;// QuickLZ data compression library +// Copyright (C) 2006-2011 Lasse Mikkel Reinhold +// lar@quicklz.com +// +// QuickLZ can be used for free under the GPL 1, 2 or 3 license (where anything +// released into public must be open source) or under a commercial license if such +// has been acquired (see http://www.quicklz.com/order.html). The commercial license +// does not cover derived or ported versions created by third parties under GPL. +// +// Only a subset of the C library has been ported, namely level 1 and 3 not in +// streaming mode. +// +// Version: 1.5.0 final + +public final class QuickLZ +{ + // Streaming mode not supported + public final static int QLZ_STREAMING_BUFFER = 0; + + // Bounds checking not supported. Use try...catch instead + public final static int QLZ_MEMORY_SAFE = 0; + + public final static int QLZ_VERSION_MAJOR = 1; + public final static int QLZ_VERSION_MINOR = 5; + public final static int QLZ_VERSION_REVISION = 0; + + // Decrease QLZ_POINTERS_3 to increase compression speed of level 3. Do not + // edit any other constants! + private final static int HASH_VALUES = 4096; + private final static int MINOFFSET = 2; + private final static int UNCONDITIONAL_MATCHLEN = 6; + private final static int UNCOMPRESSED_END = 4; + private final static int CWORD_LEN = 4; + private final static int DEFAULT_HEADERLEN = 9; + private final static int QLZ_POINTERS_1 = 1; + private final static int QLZ_POINTERS_3 = 16; + + static int headerLen(byte[] source) + { + return ((source[0] & 2) == 2) ? 9 : 3; + } + + static public long sizeDecompressed(byte[] source) + { + if (headerLen(source) == 9) + return fast_read(source, 5, 4); + else + return fast_read(source, 2, 1); + } + + static public long sizeCompressed(byte[] source) + { + if (headerLen(source) == 9) + return fast_read(source, 1, 4); + else + return fast_read(source, 1, 1); + } + + private static void write_header(byte[] dst, int level, boolean compressible, int size_compressed, int size_decompressed) + { + dst[0] = (byte)(2 | (compressible ? 1 : 0)); + dst[0] |= (byte)(level << 2); + dst[0] |= (1 << 6); + dst[0] |= (0 << 4); + fast_write(dst, 1, size_decompressed, 4); + fast_write(dst, 5, size_compressed, 4); + } + + public static byte[] compress(byte[] source, int level) + { + int src = 0; + int dst = DEFAULT_HEADERLEN + CWORD_LEN; + long cword_val = 0x80000000L; + int cword_ptr = DEFAULT_HEADERLEN; + byte[] destination = new byte[source.length + 400]; + int[][] hashtable; + int[] cachetable = new int[HASH_VALUES]; + byte[] hash_counter = new byte[HASH_VALUES]; + byte[] d2; + int fetch = 0; + int last_matchstart = (source.length - UNCONDITIONAL_MATCHLEN - UNCOMPRESSED_END - 1); + int lits = 0; + + if (level != 1 && level != 3) + throw new RuntimeException("Java version only supports level 1 and 3"); + + if (level == 1) + hashtable = new int[HASH_VALUES][QLZ_POINTERS_1]; + else + hashtable = new int[HASH_VALUES][QLZ_POINTERS_3]; + + if (source.length == 0) + return new byte[0]; + + if (src <= last_matchstart) + fetch = (int)fast_read(source, src, 3); + + while (src <= last_matchstart) + { + if ((cword_val & 1) == 1) + { + if (src > 3 * (source.length >> 2) && dst > src - (src >> 5)) + { + d2 = new byte[source.length+DEFAULT_HEADERLEN]; + write_header(d2, level, false, source.length, source.length + DEFAULT_HEADERLEN); + System.arraycopy(source, 0, d2, DEFAULT_HEADERLEN, source.length); + return d2; + } + + fast_write(destination, cword_ptr, (cword_val >>> 1) | 0x80000000L, 4); + cword_ptr = dst; + dst += CWORD_LEN; + cword_val = 0x80000000L; + } + + if (level == 1) + { + int hash = ((fetch >>> 12) ^ fetch) & (HASH_VALUES - 1); + int o = hashtable[hash][0]; + int cache = cachetable[hash] ^ fetch; + + cachetable[hash] = fetch; + hashtable[hash][0] = src; + + if (cache == 0 && hash_counter[hash] != 0 && (src - o > MINOFFSET || (src == o + 1 && lits >= 3 && src > 3 && source[src] == source[src - 3] && source[src] == source[src - 2] && source[src] == source[src - 1] && source[src] == source[src + 1] && source[src] == source[src + 2]))) + { + cword_val = ((cword_val >>> 1) | 0x80000000L); + if (source[o + 3] != source[src + 3]) + { + int f = 3 - 2 | (hash << 4); + destination[dst + 0] = (byte)(f >>> 0 * 8); + destination[dst + 1] = (byte)(f >>> 1 * 8); + src += 3; + dst += 2; + } + else + { + int old_src = src; + int remaining = ((source.length - UNCOMPRESSED_END - src + 1 - 1) > 255 ? 255 : (source.length - UNCOMPRESSED_END - src + 1 - 1)); + + src += 4; + if (source[o + src - old_src] == source[src]) + { + src++; + if (source[o + src - old_src] == source[src]) + { + src++; + while (source[o + (src - old_src)] == source[src] && (src - old_src) < remaining) + src++; + } + } + + int matchlen = src - old_src; + + hash <<= 4; + if (matchlen < 18) + { + int f = hash | (matchlen - 2); + // Neither Java nor C# wants to inline fast_write + destination[dst + 0] = (byte)(f >>> 0 * 8); + destination[dst + 1] = (byte)(f >>> 1 * 8); + dst += 2; + } + else + { + int f = hash | (matchlen << 16); + fast_write(destination, dst, f, 3); + dst += 3; + } + } + lits = 0; + fetch = (int)fast_read(source, src, 3); + } + else + { + lits++; + hash_counter[hash] = 1; + destination[dst] = source[src]; + cword_val = (cword_val >>> 1); + src++; + dst++; + fetch = ((fetch >>> 8) & 0xffff) | ((((int)source[src + 2]) & 0xff) << 16); + } + } + else + { + fetch = (int)fast_read(source, src, 3); + + int o, offset2; + int matchlen, k, m, best_k = 0; + byte c; + int remaining = ((source.length - UNCOMPRESSED_END - src + 1 - 1) > 255 ? 255 : (source.length - UNCOMPRESSED_END - src + 1 - 1)); + int hash = ((fetch >>> 12) ^ fetch) & (HASH_VALUES - 1); + + c = hash_counter[hash]; + matchlen = 0; + offset2 = 0; + for (k = 0; k < QLZ_POINTERS_3 && (c > k || c < 0); k++) + { + o = hashtable[hash][k]; + if ((byte)fetch == source[o] && (byte)(fetch >>> 8) == source[o + 1] && (byte)(fetch >>> 16) == source[o + 2] && o < src - MINOFFSET) + { + m = 3; + while (source[o + m] == source[src + m] && m < remaining) + m++; + if ((m > matchlen) || (m == matchlen && o > offset2)) + { + offset2 = o; + matchlen = m; + best_k = k; + } + } + } + o = offset2; + hashtable[hash][c & (QLZ_POINTERS_3 - 1)] = src; + c++; + hash_counter[hash] = c; + + if (matchlen >= 3 && src - o < 131071) + { + int offset = src - o; + for (int u = 1; u < matchlen; u++) + { + fetch = (int)fast_read(source, src + u, 3); + hash = ((fetch >>> 12) ^ fetch) & (HASH_VALUES - 1); + c = hash_counter[hash]++; + hashtable[hash][c & (QLZ_POINTERS_3 - 1)] = src + u; + } + + src += matchlen; + cword_val = ((cword_val >>> 1) | 0x80000000L); + + if (matchlen == 3 && offset <= 63) + { + fast_write(destination, dst, offset << 2, 1); + dst++; + } + else if (matchlen == 3 && offset <= 16383) + { + fast_write(destination, dst, (offset << 2) | 1, 2); + dst += 2; + } + else if (matchlen <= 18 && offset <= 1023) + { + fast_write(destination, dst, ((matchlen - 3) << 2) | (offset << 6) | 2, 2); + dst += 2; + } + else if (matchlen <= 33) + { + fast_write(destination, dst, ((matchlen - 2) << 2) | (offset << 7) | 3, 3); + dst += 3; + } + else + { + fast_write(destination, dst, ((matchlen - 3) << 7) | (offset << 15) | 3, 4); + dst += 4; + } + } + else + { + destination[dst] = source[src]; + cword_val = (cword_val >>> 1); + src++; + dst++; + } + } + } + + while (src <= source.length - 1) + { + if ((cword_val & 1) == 1) + { + fast_write(destination, cword_ptr, (long)((cword_val >>> 1) | 0x80000000L), 4); + cword_ptr = dst; + dst += CWORD_LEN; + cword_val = 0x80000000L; + } + + destination[dst] = source[src]; + src++; + dst++; + cword_val = (cword_val >>> 1); + } + while ((cword_val & 1) != 1) + { + cword_val = (cword_val >>> 1); + } + fast_write(destination, cword_ptr, (long)((cword_val >>> 1) | 0x80000000L), CWORD_LEN); + write_header(destination, level, true, source.length, dst); + + d2 = new byte[dst]; + System.arraycopy(destination, 0, d2, 0, dst); + return d2; + } + + static long fast_read(byte[] a, int i, int numbytes) + { + long l = 0; + for (int j = 0; j < numbytes; j++) + l |= ((((int)a[i + j]) & 0xffL) << j * 8); + return l; + } + + static void fast_write(byte[] a, int i, long value, int numbytes) + { + for (int j = 0; j < numbytes; j++) + a[i + j] = (byte)(value >>> (j * 8)); + } + + static public byte[] decompress(byte[] source) + { + int size = (int)sizeDecompressed(source); + int src = headerLen(source); + int dst = 0; + long cword_val = 1; + byte[] destination = new byte[size]; + int[] hashtable = new int[4096]; + byte[] hash_counter = new byte[4096]; + int last_matchstart = size - UNCONDITIONAL_MATCHLEN - UNCOMPRESSED_END - 1; + int last_hashed = -1; + int hash; + int fetch = 0; + + int level = (source[0] >>> 2) & 0x3; + + if (level != 1 && level != 3) + throw new RuntimeException("Java version only supports level 1 and 3"); + + if ((source[0] & 1) != 1) + { + byte[] d2 = new byte[size]; + System.arraycopy(source, headerLen(source), d2, 0, size); + return d2; + } + + for (;;) + { + if (cword_val == 1) + { + cword_val = fast_read(source, src, 4); + src += 4; + if (dst <= last_matchstart) + { + if(level == 1) + fetch = (int)fast_read(source, src, 3); + else + fetch = (int)fast_read(source, src, 4); + } + } + + if ((cword_val & 1) == 1) + { + int matchlen; + int offset2; + + cword_val = cword_val >>> 1; + + if (level == 1) + { + hash = (fetch >>> 4) & 0xfff; + offset2 = hashtable[hash]; + + if ((fetch & 0xf) != 0) + { + matchlen = (fetch & 0xf) + 2; + src += 2; + } + else + { + matchlen = ((int)source[src + 2]) & 0xff; + src += 3; + } + } + else + { + int offset; + + if ((fetch & 3) == 0) + { + offset = (fetch & 0xff) >>> 2; + matchlen = 3; + src++; + } + else if ((fetch & 2) == 0) + { + offset = (fetch & 0xffff) >>> 2; + matchlen = 3; + src += 2; + } + else if ((fetch & 1) == 0) + { + offset = (fetch & 0xffff) >>> 6; + matchlen = ((fetch >>> 2) & 15) + 3; + src += 2; + } + else if ((fetch & 127) != 3) + { + offset = (fetch >>> 7) & 0x1ffff; + matchlen = ((fetch >>> 2) & 0x1f) + 2; + src += 3; + } + else + { + offset = (fetch >>> 15); + matchlen = ((fetch >>> 7) & 255) + 3; + src += 4; + } + offset2 = (int)(dst - offset); + } + + destination[dst + 0] = destination[offset2 + 0]; + destination[dst + 1] = destination[offset2 + 1]; + destination[dst + 2] = destination[offset2 + 2]; + + for (int i = 3; i < matchlen; i += 1) + { + destination[dst + i] = destination[offset2 + i]; + } + dst += matchlen; + + if (level == 1) + { + fetch = (int)fast_read(destination, last_hashed + 1, 3); // destination[last_hashed + 1] | (destination[last_hashed + 2] << 8) | (destination[last_hashed + 3] << 16); + while (last_hashed < dst - matchlen) + { + last_hashed++; + hash = ((fetch >>> 12) ^ fetch) & (HASH_VALUES - 1); + hashtable[hash] = last_hashed; + hash_counter[hash] = 1; + fetch = fetch >>> 8 & 0xffff | (((int)destination[last_hashed + 3]) & 0xff) << 16; + } + fetch = (int)fast_read(source, src, 3); + } + else + { + fetch = (int)fast_read(source, src, 4); + } + last_hashed = dst - 1; + } + else + { + if (dst <= last_matchstart) + { + destination[dst] = source[src]; + dst += 1; + src += 1; + cword_val = cword_val >>> 1; + + if (level == 1) + { + while (last_hashed < dst - 3) + { + last_hashed++; + int fetch2 = (int)fast_read(destination, last_hashed, 3); + hash = ((fetch2 >>> 12) ^ fetch2) & (HASH_VALUES - 1); + hashtable[hash] = last_hashed; + hash_counter[hash] = 1; + } + fetch = fetch >> 8 & 0xffff | (((int)source[src + 2]) & 0xff) << 16; + } + else + { + fetch = fetch >> 8 & 0xffff | (((int)source[src + 2]) & 0xff) << 16 | (((int)source[src + 3]) & 0xff) << 24; + } + } + else + { + while (dst <= size - 1) + { + if (cword_val == 1) + { + src += CWORD_LEN; + cword_val = 0x80000000L; + } + + destination[dst] = source[src]; + dst++; + src++; + cword_val = cword_val >>> 1; + } + return destination; + } + } + } + } +} diff --git a/src/test/java/com/github/manevolent/ts3j/KeypairGenerationTest.java b/src/test/java/com/github/manevolent/ts3j/KeypairGenerationTest.java new file mode 100644 index 0000000..f252f01 --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/KeypairGenerationTest.java @@ -0,0 +1,31 @@ +package com.github.manevolent.ts3j; + +import com.github.manevolent.ts3j.util.Ts3Crypt; +import com.github.manevolent.ts3j.util.Ts3Debugging; +import javafx.util.Pair; +import junit.framework.TestCase; + +import java.util.Base64; + +public class KeypairGenerationTest extends TestCase { + public static void main(String[] args) throws Exception { + new KeypairGenerationTest().testParser(); + } + + public void testParser() throws Exception { + byte[] staticPrivateKey = Base64.getDecoder().decode("3fjdzQxJyRWLzWPAKzbHRnfWZ2sP5HAcFm7ovWZ0iVE="); + + Pair keypair = Ts3Crypt.generateKeypair(staticPrivateKey); + + assertEquals( + "2PjdzQxJyRWLzWPAKzbHRnfWZ2sP5HAcFm7ovWZ0iVE=", + Base64.getEncoder().encodeToString(keypair.getValue()) + ); + + assertEquals( + "DDmF1AgysRHsAxegbsH1r3h4FzupxQgvHQfrOLAQRaY=", + Base64.getEncoder().encodeToString(keypair.getKey()) + ); + } + +} diff --git a/src/test/java/com/github/manevolent/ts3j/PublicKeyEncodingTest.java b/src/test/java/com/github/manevolent/ts3j/PublicKeyEncodingTest.java new file mode 100644 index 0000000..49b4545 --- /dev/null +++ b/src/test/java/com/github/manevolent/ts3j/PublicKeyEncodingTest.java @@ -0,0 +1,44 @@ +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 org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.util.Base64; + +public class PublicKeyEncodingTest extends TestCase { + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public static void main(String[] args) throws Exception { + new PublicKeyEncodingTest().testParser(); + } + + public void testParser() throws Exception { + LocalIdentity keyPair = LocalIdentity.load( + Ts3Crypt.decodePublicKey(Base64.getDecoder() + .decode("MEsDAgcAAgEgAiBpPRbTliVt9KxtIz8saYdwcnNgcwaKLbKYSpDNO87u9gIgSWWPKcSJ9P6VZKJfRdpWwcfMdJv+NA9/hXUtz1uwRVI=")), + new BigInteger(Base64.getDecoder().decode("Tj6YXM3qyRv8n25L2pH+OEJnRUl4auQf8+znjYrOmWU=")), + 2294, + 2295 + ); + + ECPoint point = Ts3Crypt.decodePublicKey(Base64.getDecoder().decode(keyPair.getPublicKeyString())); + + assertEquals( + Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(point)), + Base64.getEncoder().encodeToString(Ts3Crypt.encodePublicKey(keyPair.getPublicKey())) + ); + } + +}