diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/ExtendedFeature.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/ExtendedFeature.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7f5fd50730abc37cb9bd3a707a30b0232674d965
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/ExtendedFeature.kt
@@ -0,0 +1,68 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol
+
+inline class ExtendedFeatureName(
+  val name: String,
+)
+
+enum class ExtendedFeature {
+  SynchronizedMarkerLine,
+  SaslAuthentication,
+  SaslExternal,
+  HideInactiveNetworks,
+  PasswordChange,
+  /** IRCv3 capability negotiation, account tracking */
+  CapNegotiation,
+  /** IRC server SSL validation */
+  VerifyServerSSL,
+  /** IRC server custom message rate limits */
+  CustomRateLimits,
+  // Currently not supported
+  DccFileTransfer,
+  /** Timestamp formatting in away (e.g. %%hh:mm%%) */
+  AwayFormatTimestamp,
+  /** Whether or not the core supports auth backends. */
+  Authenticators,
+  /** Sync buffer activity status */
+  BufferActivitySync,
+  /** Core-Side highlight configuration and matching */
+  CoreSideHighlights,
+  /** Show prefixes for senders in backlog */
+  SenderPrefixes,
+  /** Supports RPC call disconnectFromCore to remotely disconnect a client */
+  RemoteDisconnect,
+  /** Transmit features as list of strings */
+  ExtendedFeatures,
+  /** Serialize message time as 64-bit */
+  LongTime,
+  /** Real Name and Avatar URL in backlog */
+  RichMessages,
+  /** Backlogmanager supports filtering backlog by messagetype */
+  BacklogFilterType,
+  /** ECDSA keys for CertFP in identities */
+  EcdsaCertfpKeys,
+  /** 64-bit IDs for messages */
+  LongMessageId,
+  /** CoreInfo dynamically updated using signals */
+  SyncedCoreInfo;
+
+  fun name(): ExtendedFeatureName = ExtendedFeatureName(name)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/Flag.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/Flag.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f524148d2ad80245fe0ae033b125d37a96d0f86f
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/Flag.kt
@@ -0,0 +1,67 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol
+
+import kotlin.experimental.or
+
+
+interface Flag<T> {
+  val value: T
+}
+
+@JvmName("toByteFlag")
+fun Set<Flag<Byte>>.toFlag(): Byte = fold(0.toByte()) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toUByteFlag")
+fun Set<Flag<UByte>>.toFlag(): UByte = fold(0.toUByte()) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toShortFlag")
+fun Set<Flag<Short>>.toFlag(): Short = fold(0.toShort()) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toUShortFlag")
+fun Set<Flag<UShort>>.toFlag(): UShort = fold(0.toUShort()) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toIntFlag")
+fun Set<Flag<Int>>.toFlag(): Int = fold(0) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toUIntFlag")
+fun Set<Flag<UInt>>.toFlag(): UInt = fold(0.toUInt()) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toLongFlag")
+fun Set<Flag<Long>>.toFlag(): Long = fold(0.toLong()) { acc, el ->
+  acc or el.value
+}
+
+@JvmName("toULongFlag")
+fun Set<Flag<ULong>>.toFlag(): ULong = fold(0.toULong()) { acc, el ->
+  acc or el.value
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/LegacyFeature.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/LegacyFeature.kt
new file mode 100644
index 0000000000000000000000000000000000000000..672974f719c9e13f3fd1e1a7d1d8207867c27187
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/LegacyFeature.kt
@@ -0,0 +1,58 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol
+
+/**
+ * A list of features that are optional in core and/or client, but need runtime checking
+ *
+ * Some features require an uptodate counterpart, but don't justify a protocol break.
+ * This is what we use this enum for. Add such features to it and check at runtime on the other
+ * side for their existence.
+ *
+ * This list should be cleaned up after every protocol break, as we can assume them to be present then.
+ */
+enum class LegacyFeature(override val value: UInt): Flag<UInt> {
+  SynchronizedMarkerLine(0x0001u),
+  SaslAuthentication(0x0002u),
+  SaslExternal(0x0004u),
+  HideInactiveNetworks(0x0008u),
+  PasswordChange(0x0010u),
+  /** IRCv3 capability negotiation, account tracking */
+  CapNegotiation(0x0020u),
+  /** IRC server SSL validation */
+  VerifyServerSSL(0x0040u),
+  /** IRC server custom message rate limits */
+  CustomRateLimits(0x0080u),
+  DccFileTransfer(0x0100u),
+  /** Timestamp formatting in away (e.g. %%hh:mm%%) */
+  AwayFormatTimestamp(0x0200u),
+  /** Whether or not the core supports auth backends. */
+  Authenticators(0x0400u),
+  /** Sync buffer activity status */
+  BufferActivitySync(0x0800u),
+  /** Core-Side highlight configuration and matching */
+  CoreSideHighlights(0x1000u),
+  /** Show prefixes for senders in backlog */
+  SenderPrefixes(0x2000u),
+  /** Supports RPC call disconnectFromCore to remotely disconnect a client */
+  RemoteDisconnect(0x4000u),
+  /** Transmit features as list of strings */
+  ExtendedFeatures(0x8000u);
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/handshake/ClientInit.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/handshake/ClientInit.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a271d523900b1ab1072cc3b1c90debc76b37cbb
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/handshake/ClientInit.kt
@@ -0,0 +1,31 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.handshake
+
+import de.kuschku.quasseldroid.protocol.ExtendedFeature
+import de.kuschku.quasseldroid.protocol.LegacyFeature
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+
+class ClientInit(
+  val clientVersion: String?,
+  val buildDate: String?,
+  val clientFeatures: Set<LegacyFeature>?,
+  val featureList: List<ExtendedFeature>?,
+)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/ChainedByteBuffer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/ChainedByteBuffer.kt
similarity index 83%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/ChainedByteBuffer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/io/ChainedByteBuffer.kt
index 871b104c4d4c8825961a7cc4af9358307bf4ff08..139b1297243b15f6036af106557dfa72aad594c1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/ChainedByteBuffer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/ChainedByteBuffer.kt
@@ -17,14 +17,9 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.io
 
-import de.kuschku.quasseldroid.util.CoroutineChannel
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.yield
 import java.nio.ByteBuffer
-import java.nio.channels.AsynchronousByteChannel
-import java.nio.channels.WritableByteChannel
 import java.util.*
 
 class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct: Boolean = false) {
@@ -53,6 +48,11 @@ class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct:
     this.size += size
   }
 
+  fun nextBuffer(length: Int = 1): ByteBuffer {
+    ensureSpace(length)
+    return bufferList.last()
+  }
+
   fun put(value: Byte) {
     ensureSpace(1)
 
@@ -96,11 +96,18 @@ class ChainedByteBuffer(private val bufferSize: Int = 1024, private val direct:
   }
 
   fun put(value: ByteBuffer) {
-    while (value.remaining() > 8) {
-      putLong(value.long)
-    }
-    while (value.hasRemaining()) {
-      put(value.get())
+    ensureSpace(value.remaining())
+
+    while (value.remaining() > 0) {
+      val buffer = bufferList.last()
+      if (buffer.remaining() >= value.remaining()) {
+        buffer.put(value)
+      } else {
+        val oldLimit = value.limit()
+        value.limit(value.position() + buffer.remaining())
+        buffer.put(value)
+        value.limit(oldLimit)
+      }
     }
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/CoroutineChannel.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt
similarity index 63%
rename from app/src/main/java/de/kuschku/quasseldroid/util/CoroutineChannel.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt
index fe3bd2b67a6ec431067a175619971bbb92e66ae9..156c6ec4ae04ba051270c4f8bd07d2401c71dcfc 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/CoroutineChannel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/CoroutineChannel.kt
@@ -17,23 +17,47 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.util
+package de.kuschku.quasseldroid.protocol.io
 
-import de.kuschku.quasseldroid.protocol.ChainedByteBuffer
+import de.kuschku.quasseldroid.util.TlsInfo
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.runInterruptible
 import java.net.InetSocketAddress
 import java.net.Socket
 import java.nio.ByteBuffer
 import java.util.concurrent.Executors
+import javax.net.ssl.SSLContext
 
 class CoroutineChannel {
   private lateinit var channel: StreamChannel
   private val writeContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
   private val readContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+  private val _tlsInfo = MutableStateFlow<TlsInfo?>(null)
+  val tlsInfo: StateFlow<TlsInfo?> get() = _tlsInfo
 
-  suspend fun connect(address: InetSocketAddress) = runInterruptible(writeContext) {
-    this.channel = StreamChannel(Socket(address.address, address.port))
+  suspend fun connect(
+    address: InetSocketAddress,
+    timeout: Int = 0,
+    keepAlive: Boolean = false,
+  ) = runInterruptible(Dispatchers.IO) {
+    val socket = Socket()
+    socket.keepAlive = keepAlive
+    socket.connect(address, timeout)
+    this.channel = StreamChannel(socket)
+  }
+
+  fun enableCompression() {
+    channel = channel.withCompression()
+  }
+
+  suspend fun enableTLS(context: SSLContext) {
+    channel = runInterruptible(writeContext) {
+      channel.withTLS(context)
+    }
+    _tlsInfo.emit(channel.tlsInfo())
   }
 
   suspend fun read(buffer: ByteBuffer): Int = runInterruptible(readContext) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/ShortSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/FixedDeflaterOutputStream.kt
similarity index 70%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/ShortSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/io/FixedDeflaterOutputStream.kt
index a7cd95cb2efb3f89ad315978454160f99b5a77d7..abdad9f25eddc40e639b1e0d094cf674f544cf4f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/ShortSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/FixedDeflaterOutputStream.kt
@@ -17,16 +17,19 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.io
 
-import java.nio.ByteBuffer
+import java.io.OutputStream
+import java.util.zip.DeflaterOutputStream
 
-object ShortSerializer : Serializer<Short> {
-  override fun serialize(buffer: ChainedByteBuffer, data: Short) {
-    buffer.putShort(data)
-  }
-
-  override fun deserialize(buffer: ByteBuffer): Short {
-    return buffer.getShort()
+class FixedDeflaterOutputStream(
+  stream: OutputStream
+) : DeflaterOutputStream(stream, true) {
+  override fun close() {
+    try {
+      super.close()
+    } finally {
+      def.end()
+    }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ReadableWrappedChannel.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/ReadableWrappedChannel.kt
similarity index 98%
rename from app/src/main/java/de/kuschku/quasseldroid/util/ReadableWrappedChannel.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/io/ReadableWrappedChannel.kt
index 6bb7c3127d629519e0fda2ab855f0127dda8155a..ea399d6e80fe15194cac9d9ba81413d704a029ac 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ReadableWrappedChannel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/ReadableWrappedChannel.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.util
+package de.kuschku.quasseldroid.protocol.io
 
 import android.util.Log
 import java.io.InputStream
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/StreamChannel.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StreamChannel.kt
similarity index 72%
rename from app/src/main/java/de/kuschku/quasseldroid/util/StreamChannel.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/io/StreamChannel.kt
index c744310e31c2e08bef2162ed3e04a50d88a584e2..28d2697ea1cd7ceca71eebc54f9fda25e72afb5e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/StreamChannel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StreamChannel.kt
@@ -17,8 +17,9 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.util
+package de.kuschku.quasseldroid.protocol.io
 
+import de.kuschku.quasseldroid.util.TlsInfo
 import java.io.Flushable
 import java.io.InputStream
 import java.io.OutputStream
@@ -26,8 +27,9 @@ import java.net.Socket
 import java.nio.ByteBuffer
 import java.nio.channels.ByteChannel
 import java.nio.channels.InterruptibleChannel
-import java.util.zip.DeflaterOutputStream
 import java.util.zip.InflaterInputStream
+import javax.net.ssl.SSLContext
+import javax.net.ssl.SSLSocket
 
 class StreamChannel constructor(
   private val socket: Socket,
@@ -37,14 +39,33 @@ class StreamChannel constructor(
   private val input = ReadableWrappedChannel(inputStream)
   private val output = WritableWrappedChannel(outputStream)
 
+  fun tlsInfo(): TlsInfo? {
+    val sslSocket = socket as? SSLSocket ?: return null
+    return TlsInfo.ofSession(sslSocket.session)
+  }
+
   fun withCompression(): StreamChannel {
     return StreamChannel(
       socket,
       InflaterInputStream(inputStream),
-      DeflaterOutputStream(outputStream),
+      FixedDeflaterOutputStream(outputStream),
     )
   }
 
+  fun withTLS(
+    context: SSLContext,
+  ): StreamChannel {
+    val sslSocket = context.socketFactory.createSocket(
+      this.socket,
+      this.socket.inetAddress.hostAddress,
+      this.socket.port,
+      true
+    ) as SSLSocket
+    sslSocket.useClientMode = true
+    sslSocket.startHandshake()
+    return StreamChannel(sslSocket)
+  }
+
   override fun close() {
     input.close()
     output.close()
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StringEncoder.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StringEncoder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..694d2291b2e9b38104292f60be6d489cfecb6da4
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StringEncoder.kt
@@ -0,0 +1,88 @@
+/*
+ * Quasseldroid 1 Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.io
+
+import java.nio.ByteBuffer
+import java.nio.CharBuffer
+import java.nio.charset.Charset
+import java.nio.charset.CoderResult
+
+class StringEncoder(charset: Charset) {
+  private val encoder = charset.newEncoder()
+  private val decoder = charset.newDecoder()
+  private val charBuffer = CharBuffer.allocate(1024)
+
+  private fun charBuffer(length: Int): CharBuffer =
+    if (length < 1024) charBuffer
+    else CharBuffer.allocate(length)
+
+  private fun encodingLength(length: Int, nullLimited: Boolean) =
+    if (nullLimited) length + 1
+    else length
+
+  private fun decodingLength(length: Int, nullLimited: Boolean) =
+    if (nullLimited) length - 1
+    else length
+
+  fun encode(data: String?, target: ChainedByteBuffer, nullLimited: Boolean = false) {
+    if (data == null) return
+
+    val charBuffer = charBuffer(encodingLength(data.length, nullLimited))
+    charBuffer.put(data)
+    if (nullLimited) charBuffer.put(0.toChar())
+    charBuffer.flip()
+    encoder.reset()
+    var result: CoderResult
+    do {
+      result = encoder.encode(charBuffer, target.nextBuffer(data.length), true)
+    } while (result == CoderResult.OVERFLOW)
+  }
+
+  fun encode(data: String?, nullLimited: Boolean = false): ByteBuffer {
+    if (data == null) return ByteBuffer.allocate(0)
+
+    val charBuffer = charBuffer(encodingLength(data.length, nullLimited))
+    charBuffer.put(data)
+    if (nullLimited) charBuffer.put(0.toChar())
+    charBuffer.flip()
+    encoder.reset()
+    return encoder.encode(charBuffer)
+  }
+
+  fun decode(source: ByteBuffer, length: Int, nullLimited: Boolean = false): String {
+    val charBuffer = charBuffer(decodingLength(length, nullLimited))
+    val oldlimit = source.limit()
+    source.limit(decodingLength(source.position() + length, nullLimited))
+    decoder.reset()
+    decoder.decode(source, charBuffer, true)
+    source.limit(oldlimit)
+    charBuffer.flip()
+    return charBuffer.toString()
+  }
+
+  fun decode(source: ByteBuffer, nullLimited: Boolean = false): String {
+    val charBuffer = charBuffer(decodingLength(source.remaining(), nullLimited))
+    source.limit(decodingLength(source.capacity(), nullLimited))
+    decoder.reset()
+    decoder.decode(source, charBuffer, true)
+    charBuffer.flip()
+    return charBuffer.toString()
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StringEncoders.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StringEncoders.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f32172bc8b410f8527719e6751244c29baa0e166
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/StringEncoders.kt
@@ -0,0 +1,30 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.io
+
+import kotlin.concurrent.getOrSet
+
+private val ascii = ThreadLocal<StringEncoder>()
+private val utf8 = ThreadLocal<StringEncoder>()
+private val utf16 = ThreadLocal<StringEncoder>()
+
+fun stringEncoderAscii() = ascii.getOrSet { StringEncoder(Charsets.ISO_8859_1) }
+fun stringEncoderUtf8() = utf8.getOrSet { StringEncoder(Charsets.UTF_8) }
+fun stringEncoderUtf16() = utf16.getOrSet { StringEncoder(Charsets.UTF_16BE) }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/WritableWrappedChannel.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/WritableWrappedChannel.kt
similarity index 98%
rename from app/src/main/java/de/kuschku/quasseldroid/util/WritableWrappedChannel.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/io/WritableWrappedChannel.kt
index ef0b3fa3c7437c807519f3ba65f9dc8e3b7cb7d0..9e7fe390c9bb90ee60208b41443f3b77af0b30a4 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/WritableWrappedChannel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/io/WritableWrappedChannel.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.util
+package de.kuschku.quasseldroid.protocol.io
 
 import java.io.OutputStream
 import java.nio.ByteBuffer
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/NoSerializerForTypeException.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/NoSerializerForTypeException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0cf1b79f732f306aab857b16a0106c6bae97aa04
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/NoSerializerForTypeException.kt
@@ -0,0 +1,42 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers
+
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import de.kuschku.quasseldroid.protocol.variant.QuasselType
+
+class NoSerializerForTypeException(
+  private val javaType: Class<*>?,
+  private val qtType: Int,
+  private val quasselType: String?,
+) : Exception() {
+  constructor(quasselType: QuasselType, javaType: Class<*>? = null) :
+    this(javaType, quasselType.qtType.id, quasselType.typeName)
+
+  constructor(qtType: QtType, javaType: Class<*>? = null) :
+    this(javaType, qtType.id, null)
+
+  constructor(qtType: Int, quasselType: String?) :
+    this(null, qtType, quasselType)
+
+  override fun toString(): String {
+    return "NoSerializerForTypeException(javaType=$javaType, qtType=${QtType.of(qtType) ?: qtType}, quasselType=${QuasselType.of(quasselType) ?: quasselType})"
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/Serializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/QtSerializer.kt
similarity index 77%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/Serializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/QtSerializer.kt
index 2d6589765231d87943d96d3de980b42bf320b9d2..1066584e25a8482991193996d2f05fde8bdde787 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/Serializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/QtSerializer.kt
@@ -17,11 +17,15 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-interface Serializer<T> {
+interface QtSerializer<T> {
+  val qtType: QtType
+  val javaType: Class<out T>
   fun serialize(buffer: ChainedByteBuffer, data: T)
   fun deserialize(buffer: ByteBuffer): T
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/ByteSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/QuasselSerializer.kt
similarity index 70%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/ByteSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/QuasselSerializer.kt
index d11a523a09cbcb2aa563ca2b01ec7c546d0bc0f6..651497420a862f00835262267b6800a046ae0cb3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/ByteSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/QuasselSerializer.kt
@@ -17,16 +17,12 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers
 
-import java.nio.ByteBuffer
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import de.kuschku.quasseldroid.protocol.variant.QuasselType
 
-object ByteSerializer : Serializer<Byte> {
-  override fun serialize(buffer: ChainedByteBuffer, data: Byte) {
-    buffer.put(data)
-  }
-
-  override fun deserialize(buffer: ByteBuffer): Byte {
-    return buffer.get()
-  }
+interface QuasselSerializer<T> : QtSerializer<T> {
+  override val qtType: QtType get() = quasselType.qtType
+  val quasselType: QuasselType
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/Serializers.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/Serializers.kt
new file mode 100644
index 0000000000000000000000000000000000000000..db203e6b367c1741f8de2f061aa8d189bc60be45
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/Serializers.kt
@@ -0,0 +1,70 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers
+
+import de.kuschku.quasseldroid.protocol.serializers.primitive.*
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import de.kuschku.quasseldroid.protocol.variant.QuasselType
+import java.util.*
+
+object Serializers {
+  private val qtSerializers = listOf<QtSerializer<*>>(
+    BoolSerializer,
+    ByteSerializer,
+    IntSerializer,
+    LongSerializer,
+    ShortSerializer,
+    UByteSerializer,
+    UIntSerializer,
+    ULongSerializer,
+    UShortSerializer,
+    StringSerializerUtf16,
+    VariantSerializer,
+    VariantMapSerializer,
+  ).associateBy(QtSerializer<*>::qtType)
+
+  private val quasselSerializers = listOf<QuasselSerializer<*>>(
+  ).associateBy(QuasselSerializer<*>::quasselType)
+
+  operator fun get(type: QtType) = qtSerializers[type]
+  operator fun get(type: QuasselType) = quasselSerializers[type]
+}
+
+@Suppress("UNCHECKED_CAST")
+inline fun <reified T> serializerFor(type: QtType): QtSerializer<T> {
+  val serializer = Serializers[type]
+    ?: throw NoSerializerForTypeException(type, T::class.java)
+  if (serializer.javaType == T::class.java) {
+    return serializer as QtSerializer<T>
+  } else {
+    throw NoSerializerForTypeException(type, T::class.java)
+  }
+}
+
+@Suppress("UNCHECKED_CAST")
+inline fun <reified T> serializerFor(type: QuasselType): QuasselSerializer<T> {
+  val serializer = Serializers[type]
+    ?: throw NoSerializerForTypeException(type, T::class.java)
+  if (serializer.javaType == T::class.java) {
+    return serializer as QuasselSerializer<T>
+  } else {
+    throw NoSerializerForTypeException(type, T::class.java)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/BoolSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/BoolSerializer.kt
similarity index 71%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/BoolSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/BoolSerializer.kt
index ff6758462fd83aafbf74fadf0d91daa7da8fee96..30af9a947325b4630803e2ae2dad86bc840d39d2 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/BoolSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/BoolSerializer.kt
@@ -17,11 +17,17 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object BoolSerializer : Serializer<Boolean> {
+object BoolSerializer : QtSerializer<Boolean> {
+  override val qtType: QtType = QtType.Bool
+  override val javaType: Class<Boolean> = Boolean::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: Boolean) {
     buffer.put(
       if (data) 0x01.toByte()
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ByteSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ByteSerializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7235b80c2d927b25e80c6a955fa99fcff97375b0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ByteSerializer.kt
@@ -0,0 +1,38 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import java.nio.ByteBuffer
+
+object ByteSerializer : QtSerializer<Byte> {
+  override val qtType: QtType = QtType.Char
+  override val javaType: Class<Byte> = Byte::class.java
+
+  override fun serialize(buffer: ChainedByteBuffer, data: Byte) {
+    buffer.put(data ?: 0)
+  }
+
+  override fun deserialize(buffer: ByteBuffer): Byte {
+    return buffer.get()
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/IntSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/IntSerializer.kt
similarity index 68%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/IntSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/IntSerializer.kt
index c414476e5b3eea866ba057e800d5e97bba04540e..9962005954a98e7beb77a122fe500d61f1822bef 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/IntSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/IntSerializer.kt
@@ -17,13 +17,19 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object IntSerializer : Serializer<Int> {
+object IntSerializer : QtSerializer<Int> {
+  override val qtType: QtType = QtType.Int
+  override val javaType: Class<Int> = Int::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: Int) {
-    buffer.putInt(data)
+    buffer.putInt(data ?: 0)
   }
 
   override fun deserialize(buffer: ByteBuffer): Int {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/LongSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/LongSerializer.kt
similarity index 68%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/LongSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/LongSerializer.kt
index 334218595faf6a3a49a383715856300dccf896dc..31f09503163b0c0cd682ceb41220092a79febbaf 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/LongSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/LongSerializer.kt
@@ -17,13 +17,19 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object LongSerializer : Serializer<Long> {
+object LongSerializer : QtSerializer<Long> {
+  override val qtType: QtType = QtType.Long
+  override val javaType: Class<Long> = Long::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: Long) {
-    buffer.putLong(data)
+    buffer.putLong(data?: 0)
   }
 
   override fun deserialize(buffer: ByteBuffer): Long {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/ProtocolInfoSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ProtocolInfoSerializer.kt
similarity index 75%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/ProtocolInfoSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ProtocolInfoSerializer.kt
index e4048406798dba29fca52cd45f28f6823df55e9b..60fe5d2c28a9edf441d24aaddddaecced2fb0835 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/ProtocolInfoSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ProtocolInfoSerializer.kt
@@ -17,12 +17,18 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
 import de.kuschku.quasseldroid.ProtocolInfo
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object ProtocolInfoSerializer : Serializer<ProtocolInfo> {
+object ProtocolInfoSerializer : QtSerializer<ProtocolInfo> {
+  override val qtType: QtType = QtType.UserType
+  override val javaType: Class<ProtocolInfo> = ProtocolInfo::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: ProtocolInfo) {
       UByteSerializer.serialize(buffer, data.flags)
       UShortSerializer.serialize(buffer, data.data)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ShortSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ShortSerializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..aeb0e5dd52663cf5a70d8729486f9aad43584405
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ShortSerializer.kt
@@ -0,0 +1,38 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import java.nio.ByteBuffer
+
+object ShortSerializer : QtSerializer<Short> {
+  override val qtType: QtType = QtType.Short
+  override val javaType: Class<Short> = Short::class.java
+
+  override fun serialize(buffer: ChainedByteBuffer, data: Short) {
+    buffer.putShort(data)
+  }
+
+  override fun deserialize(buffer: ByteBuffer): Short {
+    return buffer.getShort()
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerAscii.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerAscii.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3f2d326d051869afbd4a6829038f3723544a19d0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerAscii.kt
@@ -0,0 +1,50 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.io.stringEncoderAscii
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import java.nio.ByteBuffer
+
+object StringSerializerAscii : QtSerializer<String?> {
+  override val qtType = QtType.QString
+  override val javaType: Class<out String> = String::class.java
+
+  override fun serialize(buffer: ChainedByteBuffer, data: String?) {
+    if (data == null) {
+        IntSerializer.serialize(buffer, -1)
+    } else {
+      val stringBuffer = stringEncoderAscii().encode(data, true)
+        IntSerializer.serialize(buffer, stringBuffer.remaining())
+      buffer.put(stringBuffer)
+    }
+  }
+
+  override fun deserialize(buffer: ByteBuffer): String? {
+    val length = IntSerializer.deserialize(buffer) - 1
+    return if (length < 0) {
+      null
+    } else {
+      stringEncoderAscii().decode(buffer, length, true)
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerUtf16.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerUtf16.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5f11ccafc1e82d3af6af9878121ba0a81f0753ad
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerUtf16.kt
@@ -0,0 +1,50 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.io.stringEncoderUtf16
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import java.nio.ByteBuffer
+
+object StringSerializerUtf16 : QtSerializer<String?> {
+  override val qtType = QtType.QString
+  override val javaType: Class<out String> = String::class.java
+
+  override fun serialize(buffer: ChainedByteBuffer, data: String?) {
+    if (data == null) {
+        IntSerializer.serialize(buffer, -1)
+    } else {
+      val stringBuffer = stringEncoderUtf16().encode(data)
+        IntSerializer.serialize(buffer, stringBuffer.remaining())
+      buffer.put(stringBuffer)
+    }
+  }
+
+  override fun deserialize(buffer: ByteBuffer): String? {
+    val length = IntSerializer.deserialize(buffer)
+    return if (length < 0) {
+      null
+    } else {
+      stringEncoderUtf16().decode(buffer, length)
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerUtf8.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerUtf8.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c400b6dc8a61c22239dd0bf66fce197a0f875ae8
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/StringSerializerUtf8.kt
@@ -0,0 +1,51 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.io.stringEncoderUtf8
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import java.nio.ByteBuffer
+
+object StringSerializerUtf8 : QtSerializer<String?> {
+  override val qtType = QtType.QString
+  override val javaType: Class<out String> = String::class.java
+
+  override fun serialize(buffer: ChainedByteBuffer, data: String?) {
+    if (data == null) {
+      IntSerializer.serialize(buffer, -1)
+    } else {
+      val stringBuffer = stringEncoderUtf8().encode(data)
+      IntSerializer.serialize(buffer, stringBuffer.remaining())
+      buffer.put(stringBuffer)
+    }
+  }
+
+  override fun deserialize(buffer: ByteBuffer): String? {
+    val length = IntSerializer.deserialize(buffer)
+    return if (length < 0) {
+      null
+    } else {
+      stringEncoderUtf8().decode(buffer, length)
+    }
+  }
+}
+
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/UByteSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UByteSerializer.kt
similarity index 70%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/UByteSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UByteSerializer.kt
index a288c1ebe4bef01c75e33d77c826fa37373241bd..221a4b528ad56743a893d2671bcc3cea7c1c966f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/UByteSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UByteSerializer.kt
@@ -17,11 +17,17 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object UByteSerializer : Serializer<UByte> {
+object UByteSerializer : QtSerializer<UByte> {
+  override val qtType: QtType = QtType.UChar
+  override val javaType: Class<UByte> = UByte::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: UByte) {
     buffer.put(data.toByte())
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/UIntSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UIntSerializer.kt
similarity index 71%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/UIntSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UIntSerializer.kt
index 535a6cf3ca800c4fae54d47b4dbf64144ff02d27..c416039a94270615d1a33928bf17c0e447fd0add 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/UIntSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UIntSerializer.kt
@@ -17,11 +17,17 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object UIntSerializer : Serializer<UInt> {
+object UIntSerializer : QtSerializer<UInt> {
+  override val qtType: QtType = QtType.UInt
+  override val javaType: Class<UInt> = UInt::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: UInt) {
     buffer.putInt(data.toInt())
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/ULongSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ULongSerializer.kt
similarity index 70%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/ULongSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ULongSerializer.kt
index 188a41874123002527e285a2f6bb79d6035e70a3..1bf483839d03997bade315ee4272dc10e0e75db7 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/ULongSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/ULongSerializer.kt
@@ -17,11 +17,17 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object ULongSerializer : Serializer<ULong> {
+object ULongSerializer : QtSerializer<ULong> {
+  override val qtType: QtType = QtType.ULong
+  override val javaType: Class<ULong> = ULong::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: ULong) {
     buffer.putLong(data.toLong())
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/UShortSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UShortSerializer.kt
similarity index 70%
rename from app/src/main/java/de/kuschku/quasseldroid/protocol/UShortSerializer.kt
rename to app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UShortSerializer.kt
index da78b5a5e66558ed69c3e85ac5212567e602df53..330da157bf16ef6798694fa40a5fb0553d64204b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/protocol/UShortSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/UShortSerializer.kt
@@ -17,11 +17,17 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid.protocol
+package de.kuschku.quasseldroid.protocol.serializers.primitive
 
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QtType
 import java.nio.ByteBuffer
 
-object UShortSerializer : Serializer<UShort> {
+object UShortSerializer : QtSerializer<UShort> {
+  override val qtType: QtType = QtType.UShort
+  override val javaType: Class<UShort> = UShort::class.java
+
   override fun serialize(buffer: ChainedByteBuffer, data: UShort) {
     buffer.putShort(data.toShort())
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/VariantMapSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/VariantMapSerializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..74cc2382bac3877ddb6c27f99fc99807249f35d3
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/VariantMapSerializer.kt
@@ -0,0 +1,50 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.variant.QVariantMap
+import de.kuschku.quasseldroid.protocol.variant.QVariant_
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import java.nio.ByteBuffer
+
+object VariantMapSerializer : QtSerializer<QVariantMap> {
+  override val qtType = QtType.QVariantMap
+  @Suppress("UNCHECKED_CAST")
+  override val javaType: Class<out QVariantMap> = Map::class.java as Class<QVariantMap>
+
+  override fun serialize(buffer: ChainedByteBuffer, data: QVariantMap) {
+    IntSerializer.serialize(buffer, data.size)
+    data.entries.forEach { (key, value) ->
+      StringSerializerUtf16.serialize(buffer, key)
+      VariantSerializer.serialize(buffer, value)
+    }
+  }
+
+  override fun deserialize(buffer: ByteBuffer): QVariantMap {
+    val result = mutableMapOf<String, QVariant_>()
+    val length = IntSerializer.deserialize(buffer)
+    for (i in 0 until length) {
+      result[StringSerializerUtf16.deserialize(buffer) ?: ""] = VariantSerializer.deserialize(buffer)
+    }
+    return result
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/VariantSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/VariantSerializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7c4f1841457930497e53991cb38f8938323b1908
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/serializers/primitive/VariantSerializer.kt
@@ -0,0 +1,75 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.serializers.primitive
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.*
+import de.kuschku.quasseldroid.protocol.variant.QVariant
+import de.kuschku.quasseldroid.protocol.variant.QVariant_
+import de.kuschku.quasseldroid.protocol.variant.QtType
+import de.kuschku.quasseldroid.protocol.variant.QuasselType
+import java.nio.ByteBuffer
+
+object VariantSerializer : QtSerializer<QVariant_> {
+  override val qtType = QtType.QVariant
+  override val javaType: Class<QVariant_> = QVariant::class.java
+
+  override fun serialize(buffer: ChainedByteBuffer, data: QVariant_) {
+    IntSerializer.serialize(buffer, data.serializer.qtType.id)
+    BoolSerializer.serialize(buffer, false)
+    if (data is QVariant.Custom && data.serializer.qtType == QtType.UserType) {
+      StringSerializerAscii.serialize(buffer, data.serializer.quasselType.typeName)
+    }
+    data.serialize(buffer)
+  }
+
+  override fun deserialize(buffer: ByteBuffer): QVariant_ {
+    val rawType = IntSerializer.deserialize(buffer)
+    val qtType = QtType.of(rawType)
+      ?: throw NoSerializerForTypeException(rawType, null)
+    // isNull, but we ignore it as it has no meaning
+    BoolSerializer.deserialize(buffer)
+
+    return if (qtType == QtType.UserType) {
+      val name = StringSerializerAscii.deserialize(buffer)
+      val quasselType = QuasselType.of(name)
+        ?: throw NoSerializerForTypeException(qtType.id, name)
+      deserialize(quasselType, buffer)
+    } else {
+      deserialize(qtType, buffer)
+    }
+  }
+
+  @Suppress("UNCHECKED_CAST")
+  private fun deserialize(type: QtType, buffer: ByteBuffer): QVariant_ {
+    val serializer = Serializers[type]
+      ?: throw NoSerializerForTypeException(type)
+    val value = serializer.deserialize(buffer)
+    return QVariant.of(value, serializer as QuasselSerializer<Any?>)
+  }
+
+  @Suppress("UNCHECKED_CAST")
+  private fun deserialize(type: QuasselType, buffer: ByteBuffer): QVariant_ {
+    val serializer = Serializers[type]
+      ?: throw NoSerializerForTypeException(type)
+    val value = serializer.deserialize(buffer)
+    return QVariant.of(value, serializer as QuasselSerializer<Any?>)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QVariant.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QVariant.kt
new file mode 100644
index 0000000000000000000000000000000000000000..77f58ce592834a1daaf0b8fc7a12634dbed8f31c
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QVariant.kt
@@ -0,0 +1,99 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.variant
+
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.QtSerializer
+import de.kuschku.quasseldroid.protocol.serializers.QuasselSerializer
+import de.kuschku.quasseldroid.protocol.serializers.primitive.IntSerializer
+import de.kuschku.quasseldroid.protocol.serializers.serializerFor
+
+typealias QVariant_ = QVariant<*>
+typealias QVariantList = List<QVariant_>
+typealias QVariantMap = Map<String, QVariant_>
+
+sealed class QVariant<T> constructor(
+  val data: T,
+  open val serializer: QtSerializer<T>,
+) {
+  class Typed<T> internal constructor(data: T, serializer: QtSerializer<T>) :
+    QVariant<T>(data, serializer) {
+    override fun toString() = "QVariant.Typed(${serializer.qtType.serializableName}, $data)"
+    override fun equals(other: Any?): Boolean {
+      if (this === other) return true
+      if (other !is Typed<*>) return false
+
+      if (data != other.data) return false
+      if (serializer.qtType != other.serializer.qtType) return false
+
+      return true
+    }
+
+    override fun hashCode(): Int {
+      var result = data?.hashCode() ?: 0
+      result = 31 * result + serializer.qtType.hashCode()
+      return result
+    }
+  }
+
+  class Custom<T> internal constructor(data: T, override val serializer: QuasselSerializer<T>) :
+    QVariant<T>(data, serializer) {
+    override fun toString() = "QVariant.Custom(${serializer.quasselType}, $data)"
+    override fun equals(other: Any?): Boolean {
+      if (this === other) return true
+      if (other !is Custom<*>) return false
+
+      if (data != other.data) return false
+      if (serializer.quasselType != other.serializer.quasselType) return false
+
+      return true
+    }
+
+    override fun hashCode(): Int {
+      var result = data?.hashCode() ?: 0
+      result = 31 * result + serializer.quasselType.hashCode()
+      return result
+    }
+  }
+
+  fun serialize(buffer: ChainedByteBuffer) {
+    serializer.serialize(buffer, data)
+  }
+
+  fun or(defValue: T): T {
+    return data ?: defValue
+  }
+
+  companion object {
+    fun <T> of(data: T, serializer: QtSerializer<T>) = Typed(data, serializer)
+    fun <T> of(data: T, serializer: QuasselSerializer<T>) = Custom(data, serializer)
+  }
+}
+
+inline fun <reified T> of(data: T, type: QtType): QVariant<T> =
+  QVariant.of(data, serializerFor(type))
+
+inline fun <reified T> of(data: T, type: QuasselType): QVariant<T> =
+  QVariant.of(data, serializerFor(type))
+
+@Suppress("UNCHECKED_CAST")
+inline fun <reified T> QVariant_.into(): QVariant<T>? =
+  if (this.serializer.javaType == T::class.java) this as QVariant<T>
+  else null
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QtType.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QtType.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f80dc2ca8c61e86d1e9f1c6d449708261bef5866
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QtType.kt
@@ -0,0 +1,119 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.variant
+
+import java.util.*
+
+enum class QtType(val id: kotlin.Int) {
+  Void(0),//, VoidSerializer),
+  Bool(1),//, BoolSerializer),
+  Int(2),//, IntSerializer),
+  UInt(3),//, UIntSerializer),
+  LongLong(4),
+  ULongLong(5),
+
+  Double(6),
+  QChar(7),//, CharSerializer),
+  QVariantMap(8),//, VariantMapSerializer),
+  QVariantList(9),//, VariantListSerializer),
+
+  QString(10),//, StringSerializer.UTF16),
+  QStringList(11),//, StringListSerializer),
+  QByteArray(12),//, ByteArraySerializer),
+
+  QBitArray(13),
+  QDate(14),
+  QTime(15),//, TimeSerializer),
+  QDateTime(16),//, DateTimeSerializer),
+  QUrl(17),
+
+  QLocale(18),
+  QRect(19),
+  QRectF(20),
+  QSize(21),
+  QSizeF(22),
+
+  QLine(23),
+  QLineF(24),
+  QPoint(25),
+  QPointF(26),
+  QRegExp(27),
+
+  QVariantHash(28),
+  QEasingCurve(29),
+
+  FirstGuiType(63),
+
+  QFont(64),
+  QPixmap(65),
+  QBrush(66),
+  QColor(67),
+  QPalette(68),
+
+  QIcon(69),
+  QImage(70),
+  QPolygon(71),
+  QRegion(72),
+  QBitmap(73),
+
+  QCursor(74),
+  QSizePolicy(75),
+  QKeySequence(76),
+  QPen(77),
+
+  QTextLength(78),
+  QTextFormat(79),
+  QMatrix(80),
+  QTransform(81),
+
+  QMatrix4x4(82),
+  QVector2D(83),
+  QVector3D(84),
+  QVector4D(85),
+
+  QQuaternion(86),
+
+  VoidStar(128),
+  Long(129),//, LongSerializer),
+  Short(130),//, ShortSerializer),
+  Char(131),//, ByteSerializer),
+  ULong(132),//, ULongSerializer),
+
+  UShort(133),//, UShortSerializer),
+  UChar(134),//, UByteSerializer),
+  Float(135),
+  QObjectStar(136),
+  QWidgetStar(137),
+
+  QVariant(138),//, VariantSerializer),
+
+  User(256),
+  UserType(127),
+  LastType(-1);
+
+  val serializableName =
+    if (name.startsWith("Q")) name
+    else name.toLowerCase(Locale.ENGLISH)
+
+  companion object {
+    private val values = values().associateBy(QtType::id)
+    fun of(id: kotlin.Int): QtType? = values[id]
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QuasselType.kt b/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QuasselType.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e38ab4ecec2223c4f46f1cadc85022ce248c04a9
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/protocol/variant/QuasselType.kt
@@ -0,0 +1,46 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.protocol.variant
+
+enum class QuasselType(
+  val typeName: String,
+  val qtType: QtType = QtType.UserType,
+) {
+  BufferId("BufferId"),
+  BufferInfo("BufferInfo"),
+  DccConfigIpDetectionMode("DccConfig::IpDetectionMode"),
+  DccConfigPortSelectionMode("DccConfig::PortSelectionMode"),
+  IrcUser("IrcUser"),
+  IrcChannel("IrcChannel"),
+  Identity("Identity"),
+  IdentityId("IdentityId"),
+  Message("Message"),
+  MsgId("MsgId"),
+  NetworkId("NetworkId"),
+  NetworkInfo("NetworkInfo"),
+  NetworkServer("Network::Server"),
+  QHostAddress("QHostAddress"),
+  PeerPtr("PeerPtr");
+
+  companion object {
+    private val values = values().associateBy(QuasselType::typeName)
+    fun of(typeName: String?): QuasselType? = values[typeName]
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/TlsInfo.kt b/app/src/main/java/de/kuschku/quasseldroid/util/TlsInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3b070eb3b0981d7d4a977e83a993eae660d73fe4
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/TlsInfo.kt
@@ -0,0 +1,71 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.util
+
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLSession
+
+data class TlsInfo(
+  val protocol: String,
+  val cipherSuite: String,
+  val keyExchangeMechanism: String?,
+  val certificateChain: List<X509Certificate>,
+) {
+
+  override fun toString(): String {
+    return "TlsInfo(protocol='$protocol', cipherSuite='$cipherSuite', keyExchangeMechanism=$keyExchangeMechanism)"
+  }
+
+  companion object {
+    private val cipherSuiteRegex13 = "TLS_(.*)".toRegex()
+    private val cipherSuiteRegex12 = "TLS_(.*)_WITH_(.*)".toRegex()
+
+    private fun cipherSuiteRegex(protocol: String): Regex =
+      if (protocol == "TLSv1.3") cipherSuiteRegex13
+      else cipherSuiteRegex12
+
+    private fun parseCipherSuite(protocol: String, cipherSuite: String): Pair<String, String?>? {
+      val match = cipherSuiteRegex(protocol)
+        .matchEntire(cipherSuite)
+        ?: return null
+
+      return if (protocol == "TLSv1.3") {
+        Pair(match.groupValues[1], null)
+      } else {
+        Pair(match.groupValues[1], match.groupValues.getOrNull(2))
+      }
+    }
+
+    fun ofSession(session: SSLSession): TlsInfo? {
+      val (cipherSuite, keyExchangeMechanism) = parseCipherSuite(
+        session.protocol,
+        session.cipherSuite,
+      ) ?: return null
+
+      return TlsInfo(
+        session.protocol,
+        cipherSuite,
+        keyExchangeMechanism,
+        session.peerCertificateChain
+          .map(javax.security.cert.X509Certificate::toJavaCertificate)
+      )
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/X509Helper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/X509Helper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0c76f12178f8abaa85a07f2ea8c3cf83d86ca169
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/X509Helper.kt
@@ -0,0 +1,34 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.util
+
+import java.io.ByteArrayInputStream
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate as javaCertificate
+import javax.security.cert.X509Certificate as javaxCertificate
+
+private val certificateFactory = CertificateFactory.getInstance("X.509")
+
+fun javaxCertificate.toJavaCertificate(): javaCertificate =
+  certificateFactory.generateCertificate(ByteArrayInputStream(this.encoded))
+    as javaCertificate
+
+fun javaCertificate.toJavaXCertificate(): javaxCertificate =
+  javaxCertificate.getInstance(this.encoded)
diff --git a/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt b/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt
index 4ebc9b8ae91cd877f5dd8ffa27905f971635334e..f5e8fb03f154c988a4c7648469d8209c3cfcca3c 100644
--- a/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt
+++ b/app/src/test/java/de/kuschku/quasseldroid/ExampleUnitTest.kt
@@ -1,14 +1,18 @@
 package de.kuschku.quasseldroid
 
-import de.kuschku.quasseldroid.protocol.ChainedByteBuffer
-import de.kuschku.quasseldroid.protocol.IntSerializer
-import de.kuschku.quasseldroid.protocol.UIntSerializer
-import de.kuschku.quasseldroid.util.CoroutineChannel
+import de.kuschku.quasseldroid.protocol.io.ChainedByteBuffer
+import de.kuschku.quasseldroid.protocol.serializers.primitive.IntSerializer
+import de.kuschku.quasseldroid.protocol.serializers.primitive.ProtocolInfoSerializer
+import de.kuschku.quasseldroid.protocol.serializers.primitive.UIntSerializer
+import de.kuschku.quasseldroid.protocol.io.CoroutineChannel
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import java.net.InetSocketAddress
 import java.nio.ByteBuffer
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLContext
+import javax.net.ssl.X509TrustManager
 
 /**
  * Example local unit test, which will execute on the development machine (host).
@@ -23,19 +27,39 @@ class ExampleUnitTest {
 
   @Test
   fun testNetworking() {
+    val context = SSLContext.getInstance("TLSv1.3")
+    context.init(null, arrayOf(object : X509TrustManager {
+      override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
+        // FIXME: accept everything
+      }
+
+      override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
+        // FIXME: accept everything
+      }
+
+      override fun getAcceptedIssuers(): Array<X509Certificate> {
+        // FIXME: accept nothing
+        return emptyArray()
+      }
+    }), null)
+
     runBlocking {
       val sizeBuffer = ByteBuffer.allocateDirect(4)
       val sendBuffer = ChainedByteBuffer(direct = true)
       val channel = CoroutineChannel()
       channel.connect(InetSocketAddress("kuschku.de", 4242))
       val readBuffer = ByteBuffer.allocateDirect(4)
-      UIntSerializer.serialize(sendBuffer, 0x42b3_3f00u)
+      UIntSerializer.serialize(sendBuffer, 0x42b3_3f00u or 0x03u)
       IntSerializer.serialize(sendBuffer, 2)
       UIntSerializer.serialize(sendBuffer, 0x8000_0000u)
       channel.write(sendBuffer)
       channel.read(readBuffer)
       readBuffer.flip()
-      println(IntSerializer.deserialize(readBuffer))
+      println(ProtocolInfoSerializer.deserialize(readBuffer))
+      println(channel.tlsInfo.value)
+      channel.enableTLS(context)
+      println(channel.tlsInfo.value)
+      channel.enableCompression()
     }
   }
 }