Suggested public repository name: tiktok-android-signing-toolkit — signing, device registration, login, tests. See docs/GITHUB_SETUP.md for description, topics, and release text.
Languages: English (this file, default) | العربية / Arabic
A research and reverse-engineering Python toolkit derived from analysis of the official TikTok Android app and real MITM captures. It is meant to help you understand how API requests are built (signing headers, device registration, login flow) — not to abuse the service.
Warning: Using these tools against TikTok servers or third-party accounts may violate Terms of Service and local laws. You are responsible for your use.
Reference capture (example): Raw_03-17-2026-03-58-36.folder
App: com.zhiliaoapp.musically — aligned with v44.3.x builds (e.g. 440301 / 440315) · Android
- Requirements
- Installation
- Project layout
- Where to run commands
- Quick start
- Command-line tools (reference)
- Environment variables
- Python library usage
- Login body password encoding
- Signing headers (overview)
- Signing pipeline —
sign()(includes extended walkthrough) - Device identifiers
- Login flow (simplified)
- Common error codes
- Tests
tools/directorydocs/directory- Sensitive files &
.gitignore - Related project: TikTokDeviceGenerator
- Technical references
- Maintainer, company & contact
- Support this project
- Python 3.10+ (latest stable recommended)
pycryptodome(for X-Argus — AES-CBC inside the engine)
cd tiktok_final
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtCurrent requirements.txt:
pycryptodome>=3.20.0
Optional test paths may import extra packages; see tests/test_all.py if import errors appear.
| Path | Purpose |
|---|---|
ttk/ |
Main Python package: signing, login, device registration, MITM helpers, etc. |
ttk/paths.py |
Defines PROJECT_ROOT, FIXTURES_DIR, WORKSPACE_ROOT, and resolve_data_path() (resolve relative paths against project root, then fixtures/). |
fixtures/ |
Shared device JSON: device_v44_3_1.json, examples (*.example.json), devices_001.json, … |
tests/ |
test_all.py — comprehensive test suite. |
.github/workflows/ |
Python CI (matrix 3.10–3.13, pip + tests/test_all.py) and CodeQL security analysis — same patterns GitHub suggests for this stack. |
docs/ |
Login-flow docs, analysis notes, implementation plans. |
tools/ |
Frida, JADX, Ghidra, APK/signature helpers, dump comparison. |
| Repository root | Thin launchers: login_client.py, device_register.py, flow.py, mitm_raw.py, feed_api_client.py, fake_login_probe.py — each delegates to python3 -m ttk.<module>. |
| Module | Role |
|---|---|
signing_engine.py |
Local X-Gorgon, X-Khronos, X-Argus, X-Ladon, X-SS-STUB. |
login_client.py |
Full login client (multi-step; captcha/IDV where applicable). |
device_register.py |
Register a new device via TikTok device_register API. |
flow.py |
Register device, then login in one flow. |
mitm_raw.py |
Parse Raw MITM folders; diff / export device fingerprint. |
fake_login_probe.py |
Dry probe (no real password) to validate headers / server response. |
feed_api_client.py |
Example signed feed request. |
device_guard.py |
Ticket guard material when keys are present. |
virtual_devices.py |
Dynamic virtual device profiles; optional JSON store under fixtures/virtual_devices.json. |
rapidapi_signer.py |
Remote signing via RapidAPI (alternative to local engine). |
tiktok_apk_sig.py |
Extract / merge sig_hash from APK into a device template. |
Run everything from the tiktok_final/ directory (the folder that contains the ttk/ package) so import ttk works.
Either:
python3 login_client.py ...(root shim), orpython3 -m ttk.login_client ...(module form).
python3 login_client.py --username USER --password "YourPassword"Default profile: fixtures/device_v44_3_1.json unless you pass --device.
python3 login_client.py --username USER --step1python3 device_register.py --out my_device.json --verbose
python3 login_client.py --device my_device.json --username USER --password "PASS"For a desktop GUI that registers devices in batch using Java + unidbg (binary payload → device_register), see the related repo TikTokDeviceGenerator — useful alongside this toolkit’s pure-Python device_register and JSON fixtures.
python3 flow.py --username USER --password "PASS"python3 -m ttk.signing_engine --url 'https://...?' --method POST --body 'a=b' --cookie '...'| Flag | Description |
|---|---|
--username |
Required. TikTok username. |
--password |
Plain password (encoded automatically). |
--password-hex |
Pre-encoded hex password. |
--device |
Path to device JSON (default: fixture profile). |
--proxy |
HTTP(S) proxy URL. |
--proxy-file |
First valid host:port:user:pass line → proxy. |
--no-proxy |
Ignore proxy files and proxy-related env vars. |
--step1 |
Run check-login-name only. |
--step2 |
Run pre_check only. |
--skip-check |
Skip step 1. |
--region-email |
Email for /passport/app/region/ hashed_id where applicable. |
--devices-batch |
JSON like devices_001.json (.devices[].record) merged into --device base. |
--batch-offset / --batch-limit |
Slice the batch. |
--proxy-rotate-file |
Per-row rotating proxy file with batch mode. |
--batch-summary-out |
Write JSON summary after batch. |
--verbose |
Verbose HTTP dump. |
--sign-backend local|rapidapi |
Local signing (default) or RapidAPI. |
--rapidapi-key |
RapidAPI key (or RAPIDAPI_KEY). |
Batch example:
python3 login_client.py --username U --step1 --devices-batch fixtures/devices_001.json| Flag | Description |
|---|---|
--base |
Base hardware profile (resolved via resolve_data_path, default fixture). |
--apk |
APK path — extract sig_hash and merge before the request. |
--extract-sig-only |
With --apk + --out: patch JSON only; no server registration. |
--virtual REGION |
Build base from virtual_devices.generate_device_profile (overrides --base). |
--out |
Output JSON (default device_<timestamp>.json in project root). |
--proxy / --proxy-file |
Proxy. |
--verbose |
Verbose. |
--allow-local-fallback |
On server zero/failure: still write random local IDs (not server-registered). |
--dump-golden DIR |
Save per-attempt request/response JSON (MITM diff). |
--golden-only DIR |
Single golden snapshot and exit. |
| Flag | Description |
|---|---|
--username / --password / --password-hex |
Credentials. |
--device |
Use existing device JSON (skip registration). |
--skip-register |
Use default fixture without registering. |
--save-device |
Where to save the device after successful registration. |
--proxy / --proxy-file |
Proxy for register + login. |
--verbose |
Verbose. |
--region-email |
Same as login_client. |
--allow-local-fallback |
Same as device_register. |
After registration, the flow may call get_domains/v5/ to warm up session cookies (see flow.py).
Positional argument: path to a Raw_MM-DD-YYYY....folder directory.
| Flag | Description |
|---|---|
--list |
List passport/login-related request files. |
--dump FILE |
Parse one request file → JSON on stdout. |
--suggest FILE |
Suggest device JSON patch from one request. |
--flow-diff |
Compare request order vs login_client reference. |
--export-device OUT.json |
Merge MITM fingerprint into a template JSON. |
--template BASE.json |
Device template (e.g. fixtures/device_v44_3_1.json). |
--from-file FILE |
Specific Raw .txt instead of auto-discovery. |
| Flag | Description |
|---|---|
--device |
Device JSON (default may point to a root-level file if present). |
--username |
Fixed username; otherwise random probe_*. |
--proxy / --proxy-file / --no-proxy |
Proxy controls. |
--mitm-folder / --mitm-only / --mitm-list-repo |
Discover Raw folders near the repo. |
--verbose |
Verbose. |
--skip-region |
Skip nonce / region chain. |
--only-sign |
Print local signatures only (offline). |
--step1-only |
Stop after first check step. |
Example script that fetches the feed when a registered device file exists (by default looks for device_emulator_registered.json in the project root). See the module for required profile fields.
| Flag | Description |
|---|---|
--url |
Full URL including query string. |
--method |
HTTP method (default POST). |
--body |
Request body string. |
--cookie |
Cookie header value. |
--ts |
Unix timestamp (default: now). |
| Variable | Use |
|---|---|
RAPIDAPI_KEY / X_RAPIDAPI_KEY |
RapidAPI key when using --sign-backend rapidapi. |
TIKTOK_SIGN_BACKEND |
local or rapidapi. |
TIKTOK_PROXY_FILE |
Path to a proxy list file (host:port:user:pass per line). |
TIKTOK_PROXY / HTTP_PROXY / HTTPS_PROXY |
Proxies for TikTok HTTP calls (see login_client). |
TIKTOK_MITM_FOLDER |
Default MITM folder for some fake_login_probe discovery paths. |
from ttk.signing_engine import sign, compute_stub
sig = sign(
url="https://api16-normal-c-alisg.tiktokv.com/passport/user/login/?aid=1233&ts=1234567890",
method="POST",
body=b"username=test&password=test",
cookie="store-idc=useast5; ...",
)
# sig → X-Gorgon, X-Khronos, X-Argus, X-Ladon, X-SS-STUB (as applicable)from ttk.login_client import TikTokLoginClient, encode_password
from ttk.paths import FIXTURES_DIR, PROJECT_ROOT, resolve_data_path
client = TikTokLoginClient(device_path=None, verbose=True)
# encode_password("secret") # default XOR 0x05 (newer builds)from ttk.device_register import TikTokDeviceRegister
dr = TikTokDeviceRegister(verbose=True)
reg = dr.register()from ttk.rapidapi_signer import sign_via_rapidapi # needs RAPIDAPI_KEY in environmentUsernames/passwords in x-www-form-urlencoded bodies are XOR’d per code unit, then written as hex pairs.
- Newer builds (aligned with v44.3.15 in this repo): XOR key
0x05(LOGIN_BODY_XOR_KEYinlogin_client). - Older notes (44.3.1-style): you may need
xor_key=0x17:
from ttk.login_client import encode_password
encode_password("MyPassword123") # default 0x05
encode_password("MyPassword123", xor_key=0x17) # older style| Header | Idea | Main inputs |
|---|---|---|
X-SS-STUB |
MD5(body).upper() |
Raw body bytes |
X-Khronos |
Unix time | ts |
X-Gorgon |
RC4-like + nibble/bit mixing | Query string, STUB, cookie, ts |
X-Ladon |
SIMON-128 / PKCS7 | {ts}-{license_id}-{aid} |
X-Argus |
Protobuf + layered crypto | Query/device fields + ts |
Constants and math live in ttk/signing_engine.py; narrative docs are under docs/.
All local signing goes through ttk.signing_engine.sign(). Any change to the URL query, raw body bytes, Cookie string, or ts changes the headers.
| Argument | Role |
|---|---|
url |
Full URL including ?query=.... Only the part after ? is used for signing. |
method |
"GET" / "POST" / … — affects X-SS-STUB. |
body |
bytes or str (UTF-8 if str). Must match exactly what is hashed for STUB on the wire (see gzip note below). |
cookie |
Full Cookie header string as sent on the wire. |
ts |
Unix int; default int(time.time()). Used by Gorgon, Ladon, Argus. |
- Resolve
ts(now if omitted). query_string←urlparse(url).query.body_bytes← normalized body.X-SS-STUB← if body is non-empty and method is notGET:MD5(body_bytes).hexdigest().upper(); otherwise omit from the result dict.compute_gorgon(query_string, stub, cookie, ts)→X-Gorgon,X-Khronos(stringifiedts).compute_argus(query_string, stub, ts)— needs pycryptodome; returns""if missing.compute_ladon(ts)→X-Ladon(embeds 4 random bytes unless you override at API level).- Merge final dict:
X-Khronos,X-Gorgon,X-Ladon,X-Argus, plusX-SS-STUBwhen present.
flowchart LR
subgraph inputs [Inputs]
U[url]
M[method]
B[body]
C[cookie]
T[ts]
end
subgraph sign_fn [sign]
ST[compute_stub]
GO[compute_gorgon]
AR[compute_argus]
LA[compute_ladon]
OUT[header dict]
end
U --> GO
U --> AR
B --> ST
M --> ST
ST --> GO
ST --> AR
C --> GO
T --> GO
T --> AR
T --> LA
GO --> OUT
AR --> OUT
LA --> OUT
ST --> OUT
- First 4 bytes from
MD5(query_string)(UTF-8). - Next 4 bytes from the 32-char hex
X-SS-STUB, or zeros if no STUB. - Next 4 bytes from
MD5(cookie), or zeros if empty. - 4 zero bytes, then 4 bytes from the 8-hex-digit encoding of
ts→ 20 bytes total. - Run an RC4-like KSA + PRGA, then
_gorgon_handle(nibble swap, bit reverse, XOR). - Prefix with version
8404(and per-version constants) + 40 hex chars from the 20-byte state → commonly 52 characters for8404….
Byte-perfect parity with the shipping app may still depend on binary-internal constants; this engine matches the documented dependency chain (query / stub / cookie /
ts).
- Plaintext:
"{ts}-{license_id}-{aid}"(defaults insidecompute_ladon). rand= 4 random bytes.- Key schedule from
MD5(rand + str(aid)), then SIMON-128-style block encryption with PKCS7 on the plaintext. - Output:
base64(rand ‖ ciphertext).
- Parse query into fields (
device_id,version_name,channel,os_version, …). body_hash= first 6 bytes of SM3 over raw STUB bytes (16 bytes from hex STUB);query_hashfrom SM3 over the query string.- Pack a custom protobuf-like structure (field numbers as in code), including
channelmatching the query. - Encode → PKCS7 → SIMON encrypt → XOR + reverse layer → fixed prefixes.
- AES-CBC with key/IV derived via MD5 from
_ARGUS_SIGN_KEYslices. - Base64 output with the
\xf2\x81+ ciphertext framing used by the app.
- Default
sign(): STUB = MD5 of the body bytes you pass (typically uncompressed JSON/form). device_registercan send gzip on the wire; thejava_gzip_sigpath documents STUB as MD5 of gzip bytes. When emulating that, pass the same byte sequence intosign()that the app hashes.
With --sign-backend rapidapi, X-Gorgon / X-Ladon / X-Argus / X-Khronos come from a remote API; X-SS-STUB is still computed locally (compute_stub) when a body exists. See ttk/rapidapi_signer.py and RAPIDAPI_KEY.
- Unified API:
sign()— roughly lines 559–603 inttk/signing_engine.py. - Granular helpers:
compute_stub,compute_gorgon,compute_ladon,compute_argus.
The following mirrors ttk/signing_engine.py: stdlib only, plus pycryptodome for AES-CBC inside Argus.
| Primitive | Role |
|---|---|
| MD5 | Query/cookie strings (UTF-8); STUB preimage bytes; Argus AES key/IV material (MD5 of 16-byte slices of _ARGUS_SIGN_KEY). |
| SM3 (GB/T 32905-2016) | Pure-Python _SM3; Argus body_hash / query_hash (first 6 bytes each); fixed _ARGUS_SM3_OUTPUT seeds Argus SIMON keys. |
| SIMON-128/128 | 72-round block cipher (simon_enc); Argus encrypts each 16-byte protobuf block; Ladon uses a custom multi-round Feistel schedule per block. |
| AES-128-CBC | Final Argus envelope (Crypto.Cipher.AES). |
| Protobuf (minimal) | _ProtoBuf: int → varint, str/bytes → length-delimited, nested dict → sub-message. |
| PKCS#7 | Pad to multiple of 16 before SIMON/AES. |
- In
sign(), ifmethod.upper() == "GET"or body is empty → omitX-SS-STUBfrom the returned dict. - Normalize
body:str→utf-8; keepbytesunchanged. stub_lower = MD5(body_bytes).hexdigest()(32 lowercase hex chars from hashlib).- Exposed header:
X-SS-STUB = stub_lower.upper()(32 uppercase hex characters). - Downstream use:
compute_gorgontakes that uppercase string and reads the first 8 hex pairs as bytes 4–7 of the 20-byte Gorgon state.compute_argusconverts the stub string withbytes.fromhex(stub)when non-empty; if Argus is called with no stub, it uses 16 zero bytes for SM3 input (stub_bytes).
X-Khronosisstr(ts)— the same integer Unix timestamp embedded in bytes 16–19 of the Gorgon state.- Always keep
X-Khronosconsistent with thetspassed intosign(..., ts=)when reproducing a capture.
- Choose Gorgon version string (default
8404). Load 8 key bytes from_GORGON_HEX_STR[version](8404→ all zeros;0404uses legacy non-zero constants). - Format
tsashex(ts)[2:].zfill(8)— exactly 8 hex digits (pad with leading zeros). url_md5 = MD5(query_string.encode("utf-8")).hexdigest()— take the first 8 hex characters → 4 bytes → indices 0–3 of working state.- Bytes 4–7: from
X-SS-STUB(first 8 hex digits of the 32-char uppercase stub) or zeros if no stub. - Bytes 8–11: first 4 bytes of
MD5(cookie.encode("utf-8"))if cookie non-empty, else zeros. - Bytes 12–15: fixed zeros.
- Bytes 16–19: the four bytes implied by the 8 hex digits of
khronos_hex(two hex chars per byte). - Run
_gorgon_ksa(hex_str)→ 256-byte table (RC4-like KSA; useshex_str[i % 8]and a special case when a partial sum equals 85). - Run
_gorgon_prga(inp, table)— 20 iterations, each XORsinp[i]with a byte derived from the evolving table (stream-like). - Run
_gorgon_handle(inp)on the 20 bytes: per index i: nibble-swap current byte; XOR with next byte (wrap at 20); reverse bits of that; XOR 20; bitwise~; mask0xFF. sig= concatenation of the 20 resulting bytes as two lowercase hex digits each → 40 hex chars.- Final header =
version+ four hex octets taken from fixed indices ofhex_str(hex_str[7],[3],[1],[6]formatted as02x) +sig. For8404with zero key table, the middle four octets are often00000000, giving a 52-character string pattern840400000000+ 40.
- Resolve
ts(default now),license_id(default 1611921764),aid(default 1233). - Build UTF-8 plaintext
b"{ts}-{license_id}-{aid}". - Draw
rand = os.urandom(4)unless the caller passesrand=intocompute_ladon(the publicsign()does not expose this — each call is random). keygen_bytes = rand + str(aid).encode("ascii").md5hex_bytes = MD5(keygen_bytes).hexdigest().encode("ascii")(32-byte ASCII hex).- PKCS7-pad the plaintext to a multiple of 16 bytes.
_ladon_keyscheduleexpandsmd5hex_bytesinto a 272+ byte table of 64-bit subkeys (outer loop0x22times).- For each 16-byte block, apply 34 (
0x22) Feistel rounds using subkeys from the schedule (see_ladon_encrypt_data— two 64-bit halves per block, little-endian). - Output =
rand (4 bytes) ‖ ciphertext, thenbase64→X-Ladon.
If pycryptodome is missing → return empty string.
parse_qs(query_string)— use first value for each key (_qp).channel = query["channel"]or"googleplay"— must match the real app channel in the URL when comparing to traffic.stub_bytes:bytes.fromhex(stub)if stub provided, else 16 zero bytes.body_hash = SM3(stub_bytes)[:6](6 bytes).query_hash = SM3(query_string.encode("utf-8") if query_string else bytes(16))[:6].- Fill protobuf
beanwith fields 1,2,3,4,5,6,7,8,9,10,11,12,13,14,20,23 (and optional 16sec_device_id) exactly as in source — notably 12:ts << 1, 13/14: 6-byte hashes, 23: nested map with device_type, os_version, channel,_argus_calculate_constant(os_version)(weighted sum of up to 6 digits from a normalizedos_versionstring). pb_raw = _ProtoBuf(bean).to_bytes().pb_padded = aes_pad(pb_raw, 16)(PKCS7).- SIMON-128 encrypt every 16-byte block: key words from
_ARGUS_SM3_OUTPUT[:32]unpacked as four uint64 little-endian values (_argus_simon_key_list). _argus_xor_reverse(enc_pb): prependf2 f7 fc ff f2 f7 fc ff, XOR positions 8..end with repeating 8-byte XOR key from the prefix, reverse all bytes.- Wrap with prefix
a6 6e ad 9f 77 01 d0 0c 18and suffix ASCIIao. - PKCS7-pad again to 16-byte boundary.
- AES-CBC:
key = MD5(_ARGUS_SIGN_KEY[0:16]).digest(),IV = MD5(_ARGUS_SIGN_KEY[16:32]).digest(), encrypt. - Return
Base64( b"\xf2\x81" + ciphertext )as a UTF-8 string.
| Header | Uses query |
Uses body/STUB |
Uses cookie |
Uses ts |
Uses randomness |
|---|---|---|---|---|---|
X-SS-STUB |
— | yes | — | — | — |
X-Khronos |
— | — | — | yes | — |
X-Gorgon |
yes | yes (via STUB) | yes | yes | — |
X-Ladon |
— | — | — | yes | yes (4 bytes) |
X-Argus |
yes | yes (via SM3 of stub bytes) | — | yes | yes (field 3) |
- Copy the exact query string (including
channel,version_name,device_id, …). - Use the exact
Cookieheader value for Gorgon (full string). - Hash the same body bytes the app hashed (watch gzip vs plain for STUB).
- Fix
tsto the capture’s Khronos if you need identical Gorgon/Argus/Ladon timing. - Align Argus defaults (
sdk_version,sdk_version_int,license_id,aid) with the device JSON / capture. - Expect Ladon to differ run-to-run unless you patch
compute_ladon(..., rand=...)for experiments.
python3 -m ttk.signing_engine --url '...' --body '...' --cookie '...' --ts CAPTURE_TSpython3 tests/test_all.py— STUB/Khronos exact checks; Gorgon/Argus format checks against captured shapes.
In compute_argus, bean[23] is a nested dict. The minimal encoder _ProtoBuf in signing_engine.py serializes any dict value as a length-delimited sub-message (same as protobuf wire type 2 / STRING in this encoder): the parent gets field number 23, and its payload is _ProtoBuf(inner).to_bytes().
Inner map (logical):
| Inner field # | Python value source | Encoder wire type in _ProtoBuf |
|---|---|---|
| 1 | _qp("device_type") |
string → tag `(1<<3) |
| 2 | _qp("os_version") |
string → tag `(2<<3) |
| 3 | channel (from query or "googleplay") |
string → tag `(3<<3) |
| 4 | _argus_calculate_constant(os_version) int |
varint → tag `(4<<3) |
Conceptual nesting (not actual hex dump — lengths depend on strings):
[root message fields 1,2,3,…,22,23,…]
│
└── field 23 (len-delimited) ──► nested bytes:
├── field 1 (len-delim) "device_type" e.g. sdk_gphone64_arm64
├── field 2 (len-delim) "os_version" e.g. "16"
├── field 3 (len-delim) "channel" e.g. samsung_store
└── field 4 (varint) f(os_version) e.g. 32768 for "16" with default weights
_argus_calculate_constant: takes os_version string, keeps digits only, zero-pads to 6 digits, maps each digit to [20480, 2048, 20971520, 2097152, 1342177280, 134217728] and sums — used only inside field 23.4.
Empty query string: query_hash = SM3(bytes(16))[:6] (hash of 16 zero bytes), not an empty SM3 input. Example first 6 bytes of that digest (hex): 106E34A2B8C7.
Constants live at the top of tests/test_all.py. Use them to reproduce STUB, Khronos, and SM3 slices for Argus.
CAP_2913 — POST /passport/user/login/ (capture [2913])
| Item | Value (excerpt or full) |
|---|---|
X-SS-STUB |
2334A127910BFD04C800293D423DEFA7 |
X-Khronos (with ts=1773712689) |
1773712689 |
ts passed to sign() |
1773712689 |
Captured X-Gorgon (reference only) |
8404c01c000098c0cd010cdc36e96ebb4a7782bd8189fcda33c8 — tests note exact match is not expected with the open engine (key material in app binary); format 52 chars, prefix 8404, lowercase hex in output. |
First 6 bytes of SM3(stub_bytes) (stub_bytes = 16 bytes from STUB hex) |
0AD0A7F0A1F7 |
First 6 bytes of SM3(query_utf8) for CAP_2913 query string |
414D4CED13BA |
CAP_2820 — POST pre_check body (capture [2820])
| Item | Value |
|---|---|
| Body (prefix) | account_sdk_source=app&multi_login=1&mix_mode=1&username=76716a7760627637 |
X-SS-STUB |
AFAB3892DC07AA8D95D49D68B9FF63CA |
CAP_2817 — GET check_login_name_registered: no body → no X-SS-STUB in sign() output; Gorgon uses zero stub slot.
Example — reproduce STUB + Khronos in Python
from ttk.signing_engine import sign, compute_stub
from tests.test_all import CAP_2913
assert compute_stub(CAP_2913["body"].encode()) == CAP_2913["X-SS-STUB"]
sig = sign(
url=CAP_2913["url"],
method="POST",
body=CAP_2913["body"],
cookie=CAP_2913["cookie"],
ts=CAP_2913["ts"],
)
assert sig["X-Khronos"] == CAP_2913["X-Khronos"]
assert sig["X-SS-STUB"] == CAP_2913["X-SS-STUB"]| Field | Shape | Typical generation in this repo |
|---|---|---|
device_id |
~19 decimal digits | CSPRNG at registration |
iid (install_id) |
similar | CSPRNG |
openudid |
16 hex chars | random |
cdid |
UUID string | random |
See JADX references in comments inside device_register.py.
Client TikTok servers
│ │
│──[1] GET check_login_name ───────────▶ aggr16-normal.tiktokv.us
│◀──────── is_registered / … ──────────│
│ │
│──[2] POST login/pre_check ───────────▶ api16-normal-c-alisg.tiktokv.com
│◀──────── error_code: 0 … ────────────│
│ │
│──[3] POST login ─────────────────────▶ api16-normal-c-alisg.tiktokv.com
│◀──────── session_key, uid, … ────────│
The full app flow is longer (region, captcha, IDV, …). See docs/LOGIN_FLOW.md and docs/LOGIN_FLOW_V44_3_15.md.
error_code |
Meaning | What to try |
|---|---|---|
0 |
Success | — |
7 |
Rate limit | Different device / proxy / timing |
1105 |
Captcha | Solve or follow app-equivalent flow |
2046 |
Wrong password | Check encoding / account |
2048 |
Account not found | Check username |
From tiktok_final/:
python3 tests/test_all.pyCovers signing, login helpers, device registration, JSON schema, and more. If login_sessions.json exists in the project root, extra session tests run; otherwise they are skipped (friendly for public clones).
CI: On every push/PR to main, Python CI runs the same command on Ubuntu across Python 3.10–3.13. CodeQL runs static analysis on the same triggers plus a weekly schedule.
| Tool | Role |
|---|---|
frida_controller.py + frida_hook_tiktok.js |
On-device hooking / capture via Frida. |
jadx_analyzer.py |
Scan JADX output; diff constants vs ttk/signing_engine.py. |
ghidra_analyze.py |
Binary analysis (requires Ghidra / pyghidra setup). |
apk_sig_hash.py |
APK signature hashing. |
android_sig_bruteforce.py / prepare_bruteforce.py |
Signature search / prep utilities. |
compare_device_register_dump.py |
Compare golden / dump JSON files. |
Run most scripts from tools/ or pass absolute paths as documented in each file’s docstring.
| File | Contents |
|---|---|
| LOGIN_FLOW.md | Login flow parameters and detail. |
| LOGIN_FLOW_V44_3_15.md | v44.3.15 + MITM notes. |
| ANALYSIS_REPORT.md | Analysis write-up. |
| implementation_plan.md | Historical implementation plan. |
| plan_tiktok_signer.md | Historical signer plan. |
Do not commit real sessions, live proxies, private keys, or full MITM exports. Examples: login_sessions.json, proxsy.txt, golden_out/ (per repo policy), local MITM device JSON, etc.
See .gitignore for the current list.
github.com/code-root/TikTokDeviceGenerator — companion tooling focused on creating / registering many devices:
| What it is | Desktop Tkinter app; builds registration payloads via Java and unidbg (native emulation), POSTs to TikTok’s device_register endpoint (log-va.tiktokv.com/service/2/device_register/), saves device_id / install_id (IID) and metadata as JSON. |
| Strengths | Batch mode with threading, optional HTTP/SOCKS proxy, chunked output under Device/ and batch summaries under generated_devices/. |
| Docs | Bilingual README.md (English) and README.ar.md (Arabic). |
| License | MIT (see upstream repo). |
This tiktok_final repo stays centered on Python signing (ttk/signing_engine), login_client, and documented device_register flows. TikTokDeviceGenerator is a practical option when you want a GUI + unidbg path for bulk device creation; combine its JSON output with login_client --device … here if the profile shape is compatible (validate fields before production use).
Use any third-party tool only in compliance with applicable laws and TikTok’s Terms of Service.
- Hex-Rays FLIRT user guide — library identification in IDA / similar workflows.
| Developer | Mostafa Al-Bagouri |
| Company | Storage TE |
| +20 100 199 5914 |
If this toolkit is useful to you, optional support helps maintain and improve it and related work. Pick whatever works best for you.
| Channel | How to support |
|---|---|
| PayPal | paypal.me/sofaapi |
| Binance Pay / UID | 1138751298 — send from the Binance app (Pay / internal transfer when available). |
| Binance — deposit (web) | Deposit crypto (Binance) — sign in, pick the asset, then select BSC (BEP20). |
| BSC address (copy) | 0x94c5005229784d9b7df4e7a7a0c3b25a08fd57bc |
Network: Use BSC (BEP-20) only. This address is for USDT (BEP-20) and BTC on BSC (Binance-Peg / in-app “BTC” on BSC), matching the Binance deposit flow. Do not send native Bitcoin (on-chain BTC), ERC-20, or NFTs to this address.
| USDT · BSC | BTC · BSC |
|---|---|
![]() |
![]() |
Arabic documentation: README.ar.md
Disclaimer: For research and education; respect TikTok’s Terms of Service and applicable laws.

