Files
ts3java/ts3-client/docs/myteamspeak/PROTOCOL.md
ericek111 a7aa122728 Sign in to myTeamSpeak and import what the account synchronises
The myTeamSpeak client protocol and its end-to-end encryption are
reverse-engineered (docs/myteamspeak): an HTTP POST of a framed protobuf,
a PBKDF2-HMAC-SHA512 login token and account key, an AES-GCM wrapped
item key and AES-CTR items with a SHA-512 trailer.

Options on desktop and Settings on Android gain a myTeamSpeak page: sign
in and out, and the account's bookmarks and identities, each picked for
import and marked when already here. The client stays signed in the way
the official one does, keeping the derived token and key rather than the
password; each read signs in afresh, and a sign-in the server no longer
takes signs out. Nothing is written to the account.

Items are the same Item_Data as the local TeamSpeak store, so they go
through the existing decoder and importer; a bookmark chosen alone brings
the identity it connects with, found by UUID or, as some name it, by
the identity's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:07:17 +00:00

5.0 KiB

myTeamSpeak client protocol (reverse-engineered)

Recovered from the official TS3 client binaries (TeamSpeak3-Client-linux_amd64/ts3client_linux_amd64 and re-android/.../libteamspeak_client.so) plus a captured real login/sync session. This documents what is needed to talk to myTeamSpeak for synchronization of bookmarks/identities/etc.

Transport

Not gRPC-over-HTTP2 in the usual sense. It is HTTP/1.1 POST (cpp-httplib client) to:

https://clientapi.myteamspeak.com/<endpoint>

Endpoints: authentication, session, synchronization, user, integration, messenger, tschat, addon. Headers: Content-Type: application/ts3cloud, Accept: */*, Connection: close, User-Agent: cpp-httplib/0.11.1. Cloudflare fronts it and 403s anything that doesn't look right.

Request body framing:

<1 byte: len of method name> <method-name ASCII> <serialized protobuf request>

e.g. 05 "login" <LoginData>, 12 "requestServerItems" <Sync_Request_ItemClasses>. Response body = the serialized protobuf reply (Content-Type comes back application/json but the body is protobuf, not JSON).

Services / methods (see myteamspeak.recovered.proto for full schemas)

  • LoginService (/authentication): login(LoginData) -> LoginSession, loginWithAuthToken, loginWithRenewalToken, session, deleteSession, requestAuthToken.
  • SynchronizationService (/synchronization): requestServerItems(Sync_Request_ItemClasses) -> Sync_Reply_ItemClasses (download), synchronizeItems(...) (upload/merge).

Login flow (observed)

  1. POST /authentication login with LoginData{email(1), password(2)}. password is NOT the plaintext — see Crypto below.
  2. Reply LoginSession: key(1), session(2)=UUID (bearer for later calls), limits(3), uuid(4), error(5)=200 on success (ErrorCommon.ERROR_LOGIN_OK), purge(6), username(8), myts_id_data(9)=MyTeamSpeakIdData, mytsid_user_cert(10), alternative_login_info(16).renewal_token.
  3. POST /synchronization requestServerItems with {session(1), classes(2), globalversion(3)}. Advertise each local item as {item_uuid(1), item_version(2)} where item_version is exactly the local plaintext Item_Data.sync_version_uuid; the initial request builder does not attach blobs.
  4. Reconcile the server's comparison reply. Decrypt any returned item_blob, honor deletion/not-in-db details, and retain its opaque global and per-class version cursors.
  5. If local changes are requested, POST synchronizeItems with those reply cursors. Adds/changes carry the full encrypted Item_Data frame; deletions use the detail deletion flag. See CRYPTO_RE_SALT.md for the recovered state machine and confidence boundaries.

Crypto status

myTeamSpeak sync is end-to-end encrypted, using TeamSpeak's own teamcrypto library (AES-256-GCM + SHA-2 via mbedtls + a CTR-DRBG), statically linked — NOT OpenSSL's KDFs and NOT libsodium. This is why the sync store on disk is plaintext but the wire blobs are not.

Recovered constructions:

  • The login password field is base64 of 48 bytes from PBKDF2-HMAC-SHA512 with 10,000 iterations: password=password, salt=asciiLower(email) + "ts3Login" + password.
  • The account wrapping key is the same KDF with salt purpose ts3Encryption and a 32-byte output.
  • LoginSession.key v2 is 02 || GCM-tag[16] || IV[12] || ciphertext[32], AES-256-GCM without AAD. Its plaintext is the 32-byte item key.
  • MyTeamSpeakIdData: user_public_key(1) = 32 bytes (X25519/Ed25519), user_private_key(2).encrypted_user_private_key = 112 bytes (the account private key, wrapped by a password-derived key), account_creation_time(3), my_teamspeak_id(4) = 01 20 <32 bytes>, public_signature(5).
  • Account_Data / Auth_Token_Package show the recovery model: encrypted_encryption_password, encryption_tag, iv — a random data key wrapped by the password-derived key, and separately by the recovery key (32 bytes, base64), so the recovery key is the route to decrypt items without the password.
  • Item blobs are IV[16] || AES-256-CTR ciphertext || SHA512(plaintext). CTR increments its full 128-bit counter little-endian. The original capture contained the complete ciphertext but only the first 27 of 64 digest bytes; those bytes exactly match the recovered plaintext hash.

Remaining reverse-engineering gaps

Password login, key unwrap, item encryption/integrity framing, two-phase comparison, and the local manipulation flags are recovered. The remaining uncertainties are narrower: where ordinary edits rotate sync_version_uuid, the exact lifecycle of last_known_version, the official collision winner/merge policy, and instruction-level confirmation of the deletion-detail builder. Recovery-key import is also not yet recovered. The read-only pull is implemented in com.ts3client.myts (see IMPLEMENTATION.md); destructive upload should remain gated until the deletion mapping has a fixture or controlled integration test.