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>
82 lines
5.0 KiB
Markdown
82 lines
5.0 KiB
Markdown
# 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](CRYPTO_RE_SALT.md#5-synchronization-and-upload-state-machine) 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.
|