tiksignV1 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tiksignv1-0.1.0/Argus.py +30 -0
- tiksignv1-0.1.0/CryptoHelpers.py +14 -0
- tiksignv1-0.1.0/Gorgon.py +22 -0
- tiksignv1-0.1.0/Ladon.py +22 -0
- tiksignv1-0.1.0/MiniJson.py +7 -0
- tiksignv1-0.1.0/PKG-INFO +57 -0
- tiksignv1-0.1.0/Param.py +6 -0
- tiksignv1-0.1.0/ProtoBuf.py +21 -0
- tiksignv1-0.1.0/README.md +41 -0
- tiksignv1-0.1.0/SM3.py +25 -0
- tiksignv1-0.1.0/SignOptions.py +17 -0
- tiksignv1-0.1.0/SignRequest.py +12 -0
- tiksignv1-0.1.0/Signatures.py +19 -0
- tiksignv1-0.1.0/Signer.py +32 -0
- tiksignv1-0.1.0/Simon.py +13 -0
- tiksignv1-0.1.0/UrlEncode.py +20 -0
- tiksignv1-0.1.0/__init__.py +6 -0
- tiksignv1-0.1.0/pyproject.toml +30 -0
- tiksignv1-0.1.0/setup.cfg +4 -0
- tiksignv1-0.1.0/test_vectors.py +10 -0
- tiksignv1-0.1.0/tiksignV1.egg-info/PKG-INFO +57 -0
- tiksignv1-0.1.0/tiksignV1.egg-info/SOURCES.txt +39 -0
- tiksignv1-0.1.0/tiksignV1.egg-info/dependency_links.txt +1 -0
- tiksignv1-0.1.0/tiksignV1.egg-info/requires.txt +1 -0
- tiksignv1-0.1.0/tiksignV1.egg-info/top_level.txt +1 -0
tiksignv1-0.1.0/Argus.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import base64, hashlib
|
|
2
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
3
|
+
from . import SM3, Simon
|
|
4
|
+
from .CryptoHelpers import *
|
|
5
|
+
from .UrlEncode import parseQueryParams
|
|
6
|
+
from .ProtoBuf import Field, serialize
|
|
7
|
+
SIGN_KEY=bytes.fromhex('ac1adaae95a7af94a5114ab3b3a97dd80050aa0a39314c40528caec95256c28c')
|
|
8
|
+
SM3_OUTPUT=bytes.fromhex('fc78e0a9657a0c748ce51559903ccf03510e51d3cff232d71343e88a321c5304')
|
|
9
|
+
MASK=0xffffffffffffffff
|
|
10
|
+
def bodyHash(stub):
|
|
11
|
+
b=bytes(16) if not stub else hexDecodeString(stub); return SM3.hash(b)[:6]
|
|
12
|
+
def queryHash(query): return SM3.hash(query if query else bytes(16))[:6]
|
|
13
|
+
def _pad(b): n=16-len(b)%16; return b+bytes([n])*n
|
|
14
|
+
def _enc_pb(data):
|
|
15
|
+
pb=_pad(data); keys=[le64(SM3_OUTPUT,i*8) for i in range(4)]; enc=bytearray()
|
|
16
|
+
for i in range(0,len(pb),16):
|
|
17
|
+
a,b=Simon.encrypt((le64(pb,i),le64(pb,i+8)),keys); out=bytearray(16); writeUint64LE(out,0,a); writeUint64LE(out,1,b); enc+=out
|
|
18
|
+
buf=bytearray(bytes.fromhex('f2f7fcfff2f7fcff')+enc); x=bytes(buf); xor=x[:8]; x=bytearray(x)
|
|
19
|
+
for i in range(8,len(x)): x[i]^=xor[i%8]
|
|
20
|
+
x.reverse(); payload=bytes.fromhex('a66ead9f7701d00c18')+bytes(x)+b'ao'
|
|
21
|
+
key=hashlib.md5(SIGN_KEY[:16]).digest(); iv=hashlib.md5(SIGN_KEY[16:]).digest()
|
|
22
|
+
ct=Cipher(algorithms.AES(key),modes.CBC(iv)).encryptor().update(_pad(payload))
|
|
23
|
+
return base64.b64encode(b'\xf2\x81'+ct).decode()
|
|
24
|
+
def getSign(queryhash,stub,timestamp,aid,licenseID,platform,secDeviceID,sdkVersion,sdkVersionInt,randVal=None):
|
|
25
|
+
q=parseQueryParams(queryhash); deviceID=q.get('device_id',''); versionName=q.get('version_name',''); randInt=cryptoRandUint31() if randVal is None else randVal
|
|
26
|
+
b=[Field.varint_field(1,0x20200929<<1),Field.varint_field(2,2),Field.varint_field(3,randInt&0x7fffffff),Field.string(4,str(aid).encode()),Field.string(5,deviceID.encode()),Field.string(6,str(licenseID).encode()),Field.string(7,versionName.encode()),Field.string(8,sdkVersion.encode()),Field.varint_field(9,sdkVersionInt),Field.string(10,bytes(8)),Field.varint_field(11,platform),Field.varint_field(12,timestamp<<1),Field.string(13,bodyHash(stub)),Field.string(14,queryHash(queryhash))]
|
|
27
|
+
b.append(Field.string(15,serialize([Field.varint_field(1,1),Field.varint_field(2,1),Field.varint_field(3,1),Field.varint_field(7,3348294860)])))
|
|
28
|
+
b += [Field.string(16,secDeviceID.encode()),Field.string(20,b'none'),Field.varint_field(21,738)]
|
|
29
|
+
b.append(Field.string(23,serialize([Field.string(1,b'NX551J'),Field.varint_field(2,8196),Field.varint_field(4,2162219008)])))
|
|
30
|
+
b.append(Field.varint_field(25,2)); return _enc_pb(serialize(b))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import hashlib, secrets, struct
|
|
2
|
+
|
|
3
|
+
def md5hex(value):
|
|
4
|
+
return hashlib.md5(value.encode() if isinstance(value, str) else value).hexdigest()
|
|
5
|
+
def md5hexFromBytes(value): return hashlib.md5(value).hexdigest()
|
|
6
|
+
def md5Digest(value): return hashlib.md5(value.encode() if isinstance(value, str) else value).digest()
|
|
7
|
+
def bytesToHex(value): return bytes(value).hex()
|
|
8
|
+
def hexByte(value): return f'{value & 255:02x}'
|
|
9
|
+
def firstDigestBytes(digest, n): return bytes.fromhex(digest[:2*n])
|
|
10
|
+
def hexDecodeString(value): return bytes.fromhex(value)
|
|
11
|
+
def cryptoRandByte(): return secrets.randbelow(256)
|
|
12
|
+
def cryptoRandUint31(): return secrets.randbits(31)
|
|
13
|
+
def le64(b, off=0): return struct.unpack_from('<Q', b, off)[0]
|
|
14
|
+
def writeUint64LE(dst, index, value): struct.pack_into('<Q', dst, index*8, value & 0xffffffffffffffff)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .CryptoHelpers import md5hex, firstDigestBytes, hexByte
|
|
2
|
+
def _nibble_swap(b): return ((b>>4)&15)|((b&15)<<4)
|
|
3
|
+
def _bit_reverse(b):
|
|
4
|
+
b=((b&0xf0)>>4)|((b&0x0f)<<4); b=((b&0xcc)>>2)|((b&0x33)<<2); return ((b&0xaa)>>1)|((b&0x55)<<1)
|
|
5
|
+
def _hex_unix(v): return f'{v & 0xffffffff:08x}'
|
|
6
|
+
def v1(params,payload,cookie,unix,b3,b7):
|
|
7
|
+
g=bytearray();
|
|
8
|
+
for s in (params,payload,cookie): g += firstDigestBytes(md5hex(s),4) if s else b'\0'*4
|
|
9
|
+
g += bytes((1,1,2,4)); g += firstDigestBytes(_hex_unix(unix),4)
|
|
10
|
+
val='8404'+hexByte(b7&0xf0)+hexByte(b3)+ '0000'+''.join(hexByte(x) for x in g)
|
|
11
|
+
return str(unix*1000),str(unix),val
|
|
12
|
+
def v2(params,data,cookies,unix):
|
|
13
|
+
p=bytearray()
|
|
14
|
+
for s in (params,data,cookies): p += firstDigestBytes(md5hex(s),4) if s else b'\0'*4
|
|
15
|
+
p += bytes((0,6,11,28)) + int(unix&0xffffffff).to_bytes(4,'big')
|
|
16
|
+
key=bytes.fromhex('df77b940b99b8483d1b9cbd1f7c2b985c3d0fbc3')
|
|
17
|
+
e=bytearray(a^b for a,b in zip(p,key))
|
|
18
|
+
for i in range(20):
|
|
19
|
+
f=_bit_reverse(_nibble_swap(e[i])^e[(i+1)%20]); e[i]=(f^0xff^20)&255
|
|
20
|
+
return str(unix*1000),str(unix),'840280416000'+e.hex()
|
|
21
|
+
def v3(params,data,cookies,unix):
|
|
22
|
+
a,b,c=v2(params,data,cookies,unix); return a,b,'0404b0d30000'+c[len('840280416000'):]
|
tiksignv1-0.1.0/Ladon.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from .CryptoHelpers import *
|
|
3
|
+
MASK=0xffffffffffffffff
|
|
4
|
+
def ror64(v,n): return ((v>>n)|(v<<(64-n)))&MASK
|
|
5
|
+
def paddingSize(size): return size if size%16==0 else size+(16-size%16)
|
|
6
|
+
def _pad(data,size):
|
|
7
|
+
n=16-size%16; return data[:size]+bytes([n])*n
|
|
8
|
+
def _encrypt_input(table,block):
|
|
9
|
+
d0,d1=le64(block),le64(block,8)
|
|
10
|
+
for i in range(0x22):
|
|
11
|
+
h=le64(table,i*8); d1=(h^(d0+ror64(d1,8)))&MASK; d0=(d1^ror64(d0,0x3d))&MASK
|
|
12
|
+
out=bytearray(16); writeUint64LE(out,0,d0); writeUint64LE(out,1,d1); return bytes(out)
|
|
13
|
+
def encryptLadon(md5hex_bytes,data,size):
|
|
14
|
+
table=bytearray(288); table[:32]=md5hex_bytes
|
|
15
|
+
temp=[le64(table,i*8) for i in range(4)]; b0,b8=temp[0],temp[1]; queue=[temp[2],temp[3]]+[0]*0x22
|
|
16
|
+
for i in range(0x22):
|
|
17
|
+
x9,x8=b0,b8; x8=(ror64(x8,8)+x9)&MASK; x8 ^= i; queue[2+i]=x8; x8 ^= ror64(x9,61); writeUint64LE(table,i+1,x8); b0,b8=x8,queue[i]
|
|
18
|
+
inp=_pad(data,size); return b''.join(_encrypt_input(table,inp[i:i+16]) for i in range(0,len(inp),16))
|
|
19
|
+
def encrypt(khronos,licenseID,aid,randomBytes=None):
|
|
20
|
+
randomBytes=bytes(randomBytes) if randomBytes is not None else secrets.token_bytes(4)
|
|
21
|
+
data=f'{khronos}-{licenseID}-{aid}'.encode(); keygen=randomBytes[:4]+str(aid).encode(); mh=md5hexFromBytes(keygen).encode()
|
|
22
|
+
return base64.b64encode(randomBytes[:4]+encryptLadon(mh,data,len(data))).decode()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import json
|
|
2
|
+
def parse(s): return json.loads(s)
|
|
3
|
+
def asObject(o): return o
|
|
4
|
+
def asArray(o): return o
|
|
5
|
+
def optString(m,key): return None if m.get(key) is None else str(m[key])
|
|
6
|
+
def optInt(m,key): return None if m.get(key) is None else int(m[key])
|
|
7
|
+
def optLong(m,key): return None if m.get(key) is None else int(m[key])
|
tiksignv1-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tiksignV1
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python port of the TikTok signature algorithms from the referenced Java project
|
|
5
|
+
Author: S1 / JokerPython3 Java-port contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Security :: Cryptography
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: cryptography>=41
|
|
16
|
+
|
|
17
|
+
# TikTokSignatrueForJava — Python port
|
|
18
|
+
|
|
19
|
+
This directory is a Python conversion of the Java files in `src/main/java/tiktoksignature` from the referenced repository. It preserves the six-result signing API and the deterministic override fields used by the original test vectors.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install -r requirements.txt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from tiktoksignature import Param, SignRequest, SignOptions, sign
|
|
31
|
+
|
|
32
|
+
request = SignRequest(
|
|
33
|
+
params=[Param("device_id", "7442000000000000001"), Param("version_name", "41.9.3")],
|
|
34
|
+
data=[Param("type", "3736")],
|
|
35
|
+
)
|
|
36
|
+
options = SignOptions(version=8404)
|
|
37
|
+
signatures = sign(request, options)
|
|
38
|
+
print(signatures.as_headers)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`SignRequest` accepts ordered `Param` sequences for `params`, `data`, `payload`, and `cookie`. `rawBody` can be used to supply a body string verbatim. For reproducible tests, set `timestamp`, `gorgonByte3`, `gorgonByte7`, `argusRand`, and `ladonRandom` in `SignOptions`.
|
|
42
|
+
|
|
43
|
+
## Validation
|
|
44
|
+
|
|
45
|
+
The included `test_vectors.py` compares all six outputs against the nine golden vectors from the Java repository:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
PYTHONPATH=. python -m tiktoksignature.test_vectors /path/to/TikTokSignatrueForJava/testdata
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
All nine vectors pass in this conversion.
|
|
52
|
+
|
|
53
|
+
## Modules
|
|
54
|
+
|
|
55
|
+
The Java files were converted to corresponding Python modules: `Argus.py`, `CryptoHelpers.py`, `Gorgon.py`, `Ladon.py`, `MiniJson.py`, `Param.py`, `ProtoBuf.py`, `SM3.py`, `SignOptions.py`, `SignRequest.py`, `Signatures.py`, `Signer.py`, `Simon.py`, and `UrlEncode.py`.
|
|
56
|
+
|
|
57
|
+
The implementation is intended for authorized testing and interoperability only. Keep generated signatures, cookies, and device identifiers private.
|
tiksignv1-0.1.0/Param.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
KIND_VARINT, KIND_STRING = 0, 2
|
|
3
|
+
@dataclass
|
|
4
|
+
class Field:
|
|
5
|
+
idx: int; kind: int; varint: int = 0; data: bytes = None
|
|
6
|
+
@staticmethod
|
|
7
|
+
def varint_field(idx, value): return Field(idx, KIND_VARINT, value)
|
|
8
|
+
@staticmethod
|
|
9
|
+
def string(idx, data): return Field(idx, KIND_STRING, data=data)
|
|
10
|
+
def writeVarint(vint):
|
|
11
|
+
out=bytearray(); vint &= 0xffffffff
|
|
12
|
+
while vint > 0x80:
|
|
13
|
+
out.append((vint & 0x7f)|0x80); vint >>= 7
|
|
14
|
+
out.append(vint & 0x7f); return bytes(out)
|
|
15
|
+
def serialize(fields):
|
|
16
|
+
out=bytearray()
|
|
17
|
+
for f in fields:
|
|
18
|
+
out += writeVarint((f.idx << 3) | (f.kind & 7))
|
|
19
|
+
if f.kind == KIND_VARINT: out += writeVarint(f.varint)
|
|
20
|
+
elif f.kind == KIND_STRING: out += writeVarint(len(f.data)) + f.data
|
|
21
|
+
return bytes(out)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# TikTokSignatrueForJava — Python port
|
|
2
|
+
|
|
3
|
+
This directory is a Python conversion of the Java files in `src/main/java/tiktoksignature` from the referenced repository. It preserves the six-result signing API and the deterministic override fields used by the original test vectors.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install -r requirements.txt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from tiktoksignature import Param, SignRequest, SignOptions, sign
|
|
15
|
+
|
|
16
|
+
request = SignRequest(
|
|
17
|
+
params=[Param("device_id", "7442000000000000001"), Param("version_name", "41.9.3")],
|
|
18
|
+
data=[Param("type", "3736")],
|
|
19
|
+
)
|
|
20
|
+
options = SignOptions(version=8404)
|
|
21
|
+
signatures = sign(request, options)
|
|
22
|
+
print(signatures.as_headers)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`SignRequest` accepts ordered `Param` sequences for `params`, `data`, `payload`, and `cookie`. `rawBody` can be used to supply a body string verbatim. For reproducible tests, set `timestamp`, `gorgonByte3`, `gorgonByte7`, `argusRand`, and `ladonRandom` in `SignOptions`.
|
|
26
|
+
|
|
27
|
+
## Validation
|
|
28
|
+
|
|
29
|
+
The included `test_vectors.py` compares all six outputs against the nine golden vectors from the Java repository:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
PYTHONPATH=. python -m tiktoksignature.test_vectors /path/to/TikTokSignatrueForJava/testdata
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
All nine vectors pass in this conversion.
|
|
36
|
+
|
|
37
|
+
## Modules
|
|
38
|
+
|
|
39
|
+
The Java files were converted to corresponding Python modules: `Argus.py`, `CryptoHelpers.py`, `Gorgon.py`, `Ladon.py`, `MiniJson.py`, `Param.py`, `ProtoBuf.py`, `SM3.py`, `SignOptions.py`, `SignRequest.py`, `Signatures.py`, `Signer.py`, `Simon.py`, and `UrlEncode.py`.
|
|
40
|
+
|
|
41
|
+
The implementation is intended for authorized testing and interoperability only. Keep generated signatures, cookies, and device identifiers private.
|
tiksignv1-0.1.0/SM3.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
IV=[0x7380166F,0x4914B2B9,0x172442D7,0xDA8A0600,0xA96F30BC,0x163138AA,0xE38DEE4D,0xB0FB0E4E]
|
|
3
|
+
TJ=[0x79CC4519]*16+[0x7A879D8A]*48
|
|
4
|
+
MASK=0xffffffff
|
|
5
|
+
def rotl(x,n): return ((x << (n%32)) | (x >> (32-n%32))) & MASK if n%32 else x & MASK
|
|
6
|
+
def p0(x): return x ^ rotl(x,9) ^ rotl(x,17)
|
|
7
|
+
def p1(x): return x ^ rotl(x,15) ^ rotl(x,23)
|
|
8
|
+
def hash(msg):
|
|
9
|
+
if isinstance(msg,str): msg=msg.encode()
|
|
10
|
+
m=bytearray(msg); m.append(0x80)
|
|
11
|
+
while len(m)%64 != 56: m.append(0)
|
|
12
|
+
m += struct.pack('>Q',len(msg)*8); v=IV[:]
|
|
13
|
+
for off in range(0,len(m),64):
|
|
14
|
+
w=list(struct.unpack('>16I',m[off:off+64]))
|
|
15
|
+
for j in range(16,68): w.append((p1(w[j-16]^w[j-9]^rotl(w[j-3],15))^rotl(w[j-13],7)^w[j-6])&MASK)
|
|
16
|
+
w1=[w[j]^w[j+4] for j in range(64)]
|
|
17
|
+
a,b,c,d,e,f,g,h=v
|
|
18
|
+
for j in range(64):
|
|
19
|
+
ff=(a^b^c) if j<16 else ((a&b)|(a&c)|(b&c))
|
|
20
|
+
gg=(e^f^g) if j<16 else ((e&f)|((~e)&g))
|
|
21
|
+
ss1=rotl((rotl(a,12)+e+rotl(TJ[j],j))&MASK,7); ss2=ss1^rotl(a,12)
|
|
22
|
+
tt1=(ff+d+ss2+w1[j])&MASK; tt2=(gg+h+ss1+w[j])&MASK
|
|
23
|
+
d,c,b,a=c,rotl(b,9),a,tt1; h,g,f,e=g,rotl(f,19),e,p0(tt2)&MASK
|
|
24
|
+
v=[(x^y)&MASK for x,y in zip(v,(a,b,c,d,e,f,g,h))]
|
|
25
|
+
return struct.pack('>8I',*v)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class SignOptions:
|
|
6
|
+
timestamp: Optional[int] = None
|
|
7
|
+
aid: int = 0
|
|
8
|
+
licenseID: int = 0
|
|
9
|
+
platform: int = 0
|
|
10
|
+
sdkVersion: Optional[str] = None
|
|
11
|
+
sdkVersionInt: int = 0
|
|
12
|
+
version: int = 0
|
|
13
|
+
secDeviceID: Optional[str] = None
|
|
14
|
+
gorgonByte3: Optional[int] = None
|
|
15
|
+
gorgonByte7: Optional[int] = None
|
|
16
|
+
argusRand: Optional[int] = None
|
|
17
|
+
ladonRandom: Optional[bytes] = None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Sequence
|
|
3
|
+
from .Param import Param
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class SignRequest:
|
|
7
|
+
url: Optional[str] = None
|
|
8
|
+
params: Optional[Sequence[Param]] = None
|
|
9
|
+
data: Optional[Sequence[Param]] = None
|
|
10
|
+
payload: Optional[Sequence[Param]] = None
|
|
11
|
+
cookie: Optional[Sequence[Param]] = None
|
|
12
|
+
rawBody: Optional[str] = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
@dataclass(frozen=True)
|
|
4
|
+
class Signatures:
|
|
5
|
+
xArgus: str
|
|
6
|
+
xLadon: str
|
|
7
|
+
xGorgon: str
|
|
8
|
+
xKhronos: str
|
|
9
|
+
xTTRequestTicket: str
|
|
10
|
+
xSSStub: str
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def as_headers(self):
|
|
14
|
+
return {
|
|
15
|
+
'X-Argus': self.xArgus, 'X-Ladon': self.xLadon,
|
|
16
|
+
'X-Gorgon': self.xGorgon, 'X-Khronos': self.xKhronos,
|
|
17
|
+
'X-TT-Request-Ticket': self.xTTRequestTicket,
|
|
18
|
+
'X-SS-Stub': self.xSSStub,
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import secrets, time
|
|
2
|
+
from .Param import Param
|
|
3
|
+
from .Signatures import Signatures
|
|
4
|
+
from . import UrlEncode, Gorgon, Argus, Ladon
|
|
5
|
+
|
|
6
|
+
def paramsFromURL(url):
|
|
7
|
+
query=url.split('?',1)[1] if '?' in url else ''; out=[]; seen={}
|
|
8
|
+
for p in query.split('&') if query else []:
|
|
9
|
+
k,v=(p.split('=',1)+[''])[:2] if '=' in p else (p,'')
|
|
10
|
+
if k in seen: out[seen[k]]=Param(k,v)
|
|
11
|
+
else: seen[k]=len(out); out.append(Param(k,v))
|
|
12
|
+
return out
|
|
13
|
+
|
|
14
|
+
def sign(req, opts):
|
|
15
|
+
aid=opts.aid or 1233; licenseID=opts.licenseID or 1611921764; sdkstr=opts.sdkVersion or 'v05.00.06-ov-android'; sdkint=opts.sdkVersionInt or 167775296; unix=opts.timestamp if opts.timestamp is not None else int(time.time())
|
|
16
|
+
payload=list(req.payload or []); data=list(req.data or [])
|
|
17
|
+
if not data and payload: data=payload
|
|
18
|
+
elif not payload and data: payload=data
|
|
19
|
+
params=list(req.params) if req.params is not None else paramsFromURL(req.url) if req.url else []; cookie=list(req.cookie or [])
|
|
20
|
+
payloadStr=UrlEncode.encodeParams(payload); paramsStr=UrlEncode.encodeParams(params); cookieStr=UrlEncode.encodeParams(cookie); dataStr=req.rawBody if req.rawBody is not None and req.rawBody != '' else UrlEncode.encodeParams(data)
|
|
21
|
+
stub=__import__('hashlib').md5(dataStr.encode()).hexdigest(); version=opts.version
|
|
22
|
+
if version==8404:
|
|
23
|
+
b3=opts.gorgonByte3 if opts.gorgonByte3 is not None else secrets.randbelow(256); b7=opts.gorgonByte7 if opts.gorgonByte7 is not None else secrets.randbelow(256); ticket,khronos,gorgon=Gorgon.v1(paramsStr,payloadStr,cookieStr,unix,b3,b7)
|
|
24
|
+
elif version==8402: ticket,khronos,gorgon=Gorgon.v2(paramsStr,payloadStr,cookieStr,unix)
|
|
25
|
+
elif version in (0,4404): ticket,khronos,gorgon=Gorgon.v3(paramsStr,payloadStr,cookieStr,unix)
|
|
26
|
+
else: raise ValueError(f'unsupported gorgon version {version}')
|
|
27
|
+
sec=opts.secDeviceID or ('AadCFwpTyztA5j9L'+''.join('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[x%62] for x in secrets.token_bytes(9)))
|
|
28
|
+
argus=Argus.getSign(paramsStr,stub,unix,aid,licenseID,opts.platform,sec,sdkstr,sdkint,opts.argusRand)
|
|
29
|
+
ladon=Ladon.encrypt(unix,licenseID,aid,opts.ladonRandom)
|
|
30
|
+
return Signatures(argus,ladon,gorgon,khronos,ticket,stub.upper())
|
|
31
|
+
class Signer:
|
|
32
|
+
sign=staticmethod(sign)
|
tiksignv1-0.1.0/Simon.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
MASK=0xffffffffffffffff
|
|
2
|
+
Z=0x3DC94C3A046D678B
|
|
3
|
+
def rol(v,n): return ((v<<n)|(v>>(64-n))) & MASK
|
|
4
|
+
def ror(v,n): return ((v>>n)|(v<<(64-n))) & MASK
|
|
5
|
+
def encrypt(pt,k):
|
|
6
|
+
key=list(k)+[0]*68
|
|
7
|
+
for i in range(4,72):
|
|
8
|
+
tmp=ror(key[i-1],3)^key[i-3]; tmp ^= ror(tmp,1)
|
|
9
|
+
key[i]=((~key[i-4])^tmp^((Z>>((i-4)%62))&1)^3)&MASK
|
|
10
|
+
x,y=pt
|
|
11
|
+
for i in range(72):
|
|
12
|
+
x,y=y,(x^(rol(y,1)&rol(y,8))^rol(y,2)^key[i])&MASK
|
|
13
|
+
return x,y
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from urllib.parse import quote_plus as _quote_plus
|
|
2
|
+
from .Param import Param
|
|
3
|
+
|
|
4
|
+
def quotePlus(s): return _quote_plus(str(s), safe='-_.~')
|
|
5
|
+
def unquotePlus(s):
|
|
6
|
+
from urllib.parse import unquote_plus
|
|
7
|
+
return unquote_plus(s)
|
|
8
|
+
def encodeParams(pairs):
|
|
9
|
+
return '' if pairs is None else '&'.join(f'{quotePlus(p.key)}={quotePlus(p.value)}' for p in pairs)
|
|
10
|
+
def parseQueryParams(qs):
|
|
11
|
+
out = {}
|
|
12
|
+
if not qs: return out
|
|
13
|
+
for chunk in qs.split('&'):
|
|
14
|
+
for pair in chunk.split(';'):
|
|
15
|
+
if not pair or '=' not in pair: continue
|
|
16
|
+
name, value = pair.split('=', 1)
|
|
17
|
+
if not value: continue
|
|
18
|
+
name, value = unquotePlus(name), unquotePlus(value)
|
|
19
|
+
if name not in out: out[name] = value
|
|
20
|
+
return out
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tiksignV1"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python port of the TikTok signature algorithms from the referenced Java project"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{name = "S1 / JokerPython3 Java-port contributors"}]
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Security :: Cryptography",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["cryptography>=41"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["tiktoksignature"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.package-dir]
|
|
27
|
+
tiktoksignature = "."
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.exclude-package-data]
|
|
30
|
+
"*" = ["testdata/*", "__pycache__/*", "*.pyc"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import json, pathlib, sys
|
|
2
|
+
from tiktoksignature import Param, SignRequest, SignOptions, sign
|
|
3
|
+
root=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else '../tiktoksig/testdata')
|
|
4
|
+
for path in sorted(root.glob('vector_*.json')):
|
|
5
|
+
d=json.loads(path.read_text()); i=d['input']; pairs=lambda x: None if x is None else [Param(*p) for p in x]
|
|
6
|
+
req=SignRequest(params=pairs(i.get('params')),data=pairs(i.get('data')),payload=pairs(i.get('payload')),cookie=pairs(i.get('cookie')),rawBody=i.get('raw_body'))
|
|
7
|
+
o=SignOptions(timestamp=i.get('timestamp'),aid=i.get('aid',0),licenseID=i.get('license_id',0),platform=i.get('platform',0),sdkVersion=i.get('sdk_version_str'),sdkVersionInt=i.get('sdk_version',0),version=i.get('version') or 0,secDeviceID=i.get('sec_device_id'),gorgonByte3=d.get('random',{}).get('gorgon_byte3'),gorgonByte7=d.get('random',{}).get('gorgon_byte7'),argusRand=d.get('random',{}).get('argus_rand'),ladonRandom=bytes.fromhex(d.get('random',{}).get('ladon_random','')) if d.get('random',{}).get('ladon_random') else None)
|
|
8
|
+
got=sign(req,o); exp=d['expected']; actual={'x_argus':got.xArgus,'x_ladon':got.xLadon,'x_gorgon':got.xGorgon,'x_khronos':got.xKhronos,'x_tt_request_ticket':got.xTTRequestTicket,'x_ss_stub':got.xSSStub}
|
|
9
|
+
assert actual==exp, f'{path.name} mismatch\nexpected={exp}\nactual={actual}'
|
|
10
|
+
print('PASS',path.name)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tiksignV1
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python port of the TikTok signature algorithms from the referenced Java project
|
|
5
|
+
Author: S1 / JokerPython3 Java-port contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Security :: Cryptography
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: cryptography>=41
|
|
16
|
+
|
|
17
|
+
# TikTokSignatrueForJava — Python port
|
|
18
|
+
|
|
19
|
+
This directory is a Python conversion of the Java files in `src/main/java/tiktoksignature` from the referenced repository. It preserves the six-result signing API and the deterministic override fields used by the original test vectors.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install -r requirements.txt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from tiktoksignature import Param, SignRequest, SignOptions, sign
|
|
31
|
+
|
|
32
|
+
request = SignRequest(
|
|
33
|
+
params=[Param("device_id", "7442000000000000001"), Param("version_name", "41.9.3")],
|
|
34
|
+
data=[Param("type", "3736")],
|
|
35
|
+
)
|
|
36
|
+
options = SignOptions(version=8404)
|
|
37
|
+
signatures = sign(request, options)
|
|
38
|
+
print(signatures.as_headers)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`SignRequest` accepts ordered `Param` sequences for `params`, `data`, `payload`, and `cookie`. `rawBody` can be used to supply a body string verbatim. For reproducible tests, set `timestamp`, `gorgonByte3`, `gorgonByte7`, `argusRand`, and `ladonRandom` in `SignOptions`.
|
|
42
|
+
|
|
43
|
+
## Validation
|
|
44
|
+
|
|
45
|
+
The included `test_vectors.py` compares all six outputs against the nine golden vectors from the Java repository:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
PYTHONPATH=. python -m tiktoksignature.test_vectors /path/to/TikTokSignatrueForJava/testdata
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
All nine vectors pass in this conversion.
|
|
52
|
+
|
|
53
|
+
## Modules
|
|
54
|
+
|
|
55
|
+
The Java files were converted to corresponding Python modules: `Argus.py`, `CryptoHelpers.py`, `Gorgon.py`, `Ladon.py`, `MiniJson.py`, `Param.py`, `ProtoBuf.py`, `SM3.py`, `SignOptions.py`, `SignRequest.py`, `Signatures.py`, `Signer.py`, `Simon.py`, and `UrlEncode.py`.
|
|
56
|
+
|
|
57
|
+
The implementation is intended for authorized testing and interoperability only. Keep generated signatures, cookies, and device identifiers private.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Argus.py
|
|
2
|
+
CryptoHelpers.py
|
|
3
|
+
Gorgon.py
|
|
4
|
+
Ladon.py
|
|
5
|
+
MiniJson.py
|
|
6
|
+
Param.py
|
|
7
|
+
ProtoBuf.py
|
|
8
|
+
README.md
|
|
9
|
+
SM3.py
|
|
10
|
+
SignOptions.py
|
|
11
|
+
SignRequest.py
|
|
12
|
+
Signatures.py
|
|
13
|
+
Signer.py
|
|
14
|
+
Simon.py
|
|
15
|
+
UrlEncode.py
|
|
16
|
+
__init__.py
|
|
17
|
+
pyproject.toml
|
|
18
|
+
test_vectors.py
|
|
19
|
+
./Argus.py
|
|
20
|
+
./CryptoHelpers.py
|
|
21
|
+
./Gorgon.py
|
|
22
|
+
./Ladon.py
|
|
23
|
+
./MiniJson.py
|
|
24
|
+
./Param.py
|
|
25
|
+
./ProtoBuf.py
|
|
26
|
+
./SM3.py
|
|
27
|
+
./SignOptions.py
|
|
28
|
+
./SignRequest.py
|
|
29
|
+
./Signatures.py
|
|
30
|
+
./Signer.py
|
|
31
|
+
./Simon.py
|
|
32
|
+
./UrlEncode.py
|
|
33
|
+
./__init__.py
|
|
34
|
+
./test_vectors.py
|
|
35
|
+
tiksignV1.egg-info/PKG-INFO
|
|
36
|
+
tiksignV1.egg-info/SOURCES.txt
|
|
37
|
+
tiksignV1.egg-info/dependency_links.txt
|
|
38
|
+
tiksignV1.egg-info/requires.txt
|
|
39
|
+
tiksignV1.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography>=41
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tiktoksignature
|