"""快手极速版短信登录 CLI(全新设备 -> 注册 -> 发码 -> 验码 -> 会话)。 每次执行都会现场生成设备画像并在线注册,不读取或保存历史设备、抓包字段和 区域票据。公开参数只包含登录输入和 HTTP 传输选择: uv run python -m tools.sms_login_cli --mobile 13800000000 uv run python -m tools.sms_login_cli --mobile 13800000000 --code 123456 uv run python -m tools.sms_login_cli --mobile 13800000000 --transport okhttp4-android10 """ from __future__ import annotations import argparse import json import sys import time import urllib.parse from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from core.device_cookie import device_profile_cookie_fields # noqa: E402 from core.device_profile import DeviceProfileGenerator # noqa: E402 from core.http_transport import create_http_session # noqa: E402 from core.mobile_encrypt import loginhelper_encrypt_mobile # noqa: E402 from core.sig3 import Kwsg10418State, kwsg_10418_session_seed_from_time # noqa: E402 from core.sms_login import ( # noqa: E402 DEFAULT_LOGIN_HOST, DEFAULT_REGION_HOST, LOGIN_SMS_CODE_TYPE, REGION_FULL_CONFIG_HOST, build_account_security_fields, login_api_params, login_by_code_try_paths, refresh_region_ticket, request_mobile_code, ) from core.weapon_kas import APK_DEFAULT_WEAPON_KAW, WeaponProofProvider # noqa: E402 from core.weapon_mf import generate_profile_passport_account_image # noqa: E402 from tools.new_device import run_online_bootstrap # noqa: E402 LOGIN_COUNTRY_CODE = "+86" REQUEST_TIMEOUT_SECONDS = 20 CAPTCHA_RETRIES = 2 _CAPTCHA_RESULT_HINTS = { 350001: "参数错误:轨迹或 verifyParam 为空/无效", 350002: "缺口识别或滑动落点错误", 350005: "verifyParam 中的 captchaSn 错误", 350009: "验证时间过长,captchaSn 已失效", 350014: "轨迹或环境校验未通过,需要获取新的 captchaSn", 350017: "验证参数已失效", 350029: "服务端已切换验证码类型", } _ACCOUNT_COOKIE_ORDER = ( "kpn", "kpf", "userId", "did", "c", "ver", "appver", "language", "countryCode", "sys", "mod", "net", "deviceName", "earphoneMode", "isp", "ud", "did_tag", "egid", "thermal", "kcv", "app", "bottom_navigation", "android_os", "oDid", "boardPlatform", "newOc", "androidApiLevel", "slh", "country_code", "nbh", "hotfix_ver", "did_gt", "keyconfig_state", "cdid_tag", "max_memory", "sid", "cold_launch_time_ms", "oc", "sh", "deviceBit", "browseType", "ddpi", "socName", "is_background", "sw", "ftt", "apptype", "abi", "cl", "userRecoBit", "device_abi", "icaver", "totalMemory", "grant_browse_type", "iuid", "rdid", "sbh", "darkMode", "oaid", "client_key", "kuaishou.api_st", "token", "region_ticket", "__NSWJ", "kuaishou.h5_st", "os", ) def _mobile_argument(value: str) -> str: mobile = value.strip() if not mobile.isdigit() or not 5 <= len(mobile) <= 20: raise argparse.ArgumentTypeError("手机号必须是 5-20 位数字,不能使用 MOBILE 等占位符") return mobile def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="快手极速版短信登录(每次使用全新设备)" ) parser.add_argument( "--mobile", required=True, type=_mobile_argument, help="手机号(仅数字,不含国家码)", ) parser.add_argument("--code", default="", help="短信验证码;不传则发码后交互输入") parser.add_argument( "--base-url", default="", help="登录 host(默认 apissl.ksapisrv.com)", ) parser.add_argument( "--transport", choices=("requests", "okhttp4-android10"), default="requests", help="HTTP 传输(默认 requests)", ) return parser def _new_sig3_state() -> Kwsg10418State: seed = kwsg_10418_session_seed_from_time(int(time.time())) return Kwsg10418State(session_seed=seed, counter=0x5F) def _registrable_domain(host: str) -> str: host = (host or "").strip().lower() if not host: return "" labels = host.split(".") return "." + ".".join(labels[-2:]) if len(labels) >= 2 else "." + host def _set_region_cookie(session: Any, ticket: str, *, host: str = "") -> None: if not ticket: return domain = _registrable_domain(host) if domain: session.cookies.set("region_ticket", ticket, domain=domain) session.cookies.set("__NSWJ", "", domain=domain) return session.cookies.set("region_ticket", ticket) session.cookies.set("__NSWJ", "") def _mask_mobile(value: str) -> str: mobile = str(value or "") if len(mobile) == 11 and mobile.startswith("1") and mobile.isdigit(): return f"{mobile[:3]}****{mobile[-4:]}" return mobile def _mask_secret(value: Any, *, head: int = 8, tail: int = 6) -> str: text = str(value or "") if not text: return "" if len(text) <= head + tail + 6: return f"" return f"{text[:head]}...{text[-tail:]}(len={len(text)})" def _is_secret_key(key: Any) -> bool: name = str(key or "").lower() return ( name in { "kuaishou.api_st", "kuaishou.h5_st", "kuaishou.api_client_salt", "passtoken", "quicklogintoken", "region_ticket", } or name.endswith("_st") or "token" in name or "salt" in name or name in {"api_st", "h5_st", "pass_token", "quicklogin_token", "client_salt"} ) def _looks_like_secret_text(value: str) -> bool: text = value.lower() return any( marker in text for marker in ( "kuaishou.api_st", "kuaishou.h5_st", "kuaishou.api_client_salt", "passtoken", "quicklogintoken", "pass_token", "client_salt", ) ) def _redact_for_display(value: Any, show_secrets: bool = False) -> Any: if show_secrets: return value if isinstance(value, dict): return { key: _mask_secret(item) if _is_secret_key(key) else _redact_for_display(item, show_secrets) for key, item in value.items() } if isinstance(value, list): return [_redact_for_display(item, show_secrets) for item in value] if isinstance(value, str) and _looks_like_secret_text(value): return _mask_secret(value, head=20, tail=12) return value def _json_for_display(value: Any, limit: int, *, show_secrets: bool = False) -> str: return json.dumps( _redact_for_display(value, show_secrets), ensure_ascii=False )[:limit] def _print_raw_response( raw: dict[str, Any], *, show_secrets: bool = False, body_limit: int = 400 ) -> None: body = raw.get("body") if isinstance(raw.get("body"), dict) else {} print( f" status={raw.get('status')} result={body.get('result')} " f"error_msg={body.get('error_msg')}" ) print( f" body: " f"{_json_for_display(body, body_limit, show_secrets=show_secrets)}" ) print( f" cookies: " f"{_json_for_display(raw.get('cookies') or {}, 500, show_secrets=show_secrets)}" ) print( f" set_cookie: " f"{_json_for_display(raw.get('set_cookie') or [], 500, show_secrets=show_secrets)}" ) if raw.get("error"): print(f" error: {raw.get('error')}") def _captcha_error_url(raw: dict[str, Any]) -> str: body = raw.get("body") if isinstance(raw.get("body"), dict) else {} if body.get("result") != 705: return "" error_url = str(body.get("error_url") or "").strip() return error_url if error_url.startswith("https://") else "" def _solve_captcha_http( error_url: str, *, session: Any, show_secrets: bool = False ) -> str: print("[验证] 纯 HTTP 求解(ddddocr + Jose + 合成传感器)") try: try: from tools.captcha_solver_http import solve_captcha except ImportError: from captcha_solver_http import solve_captcha except ImportError as exc: print(f"[验证] HTTP solver 导入失败: {exc}") return "" try: result = solve_captcha(session=session, error_url=error_url, verbose=True) except Exception as exc: # noqa: BLE001 print(f"[验证] HTTP solver 异常: {exc.__class__.__name__}: {exc}") return "" raw_result = result.get("raw") if isinstance(result.get("raw"), dict) else {} result_code = result.get("result") if result_code is None: result_code = raw_result.get("result") server_desc = result.get("desc") or raw_result.get("desc") or "-" token = str(result.get("captcha_token") or "") if token: shown_token = token if show_secrets else _mask_secret(token, head=10, tail=8) print( f"[验证] HTTP 求解成功 result={result_code} " f"captchaToken={shown_token}" ) else: hint = _CAPTCHA_RESULT_HINTS.get(result_code, "未分类响应") print( f"[验证] HTTP 求解未通过 result={result_code} " f"stage={result.get('stage')} desc={server_desc}" ) print(f" 诊断: {hint}") if result_code == 350029: print( f" next_type={result.get('type') or raw_result.get('type') or '-'} " f"config_path=" f"{result.get('configPath') or raw_result.get('configPath') or '-'}" ) return token def _complete_captcha_handoff( error_url: str, *, stage: str, session: Any, show_secrets: bool = False, ) -> str: print(f"[验证] {stage} 触发 705") print(f" 验证地址: {error_url}") token = _solve_captcha_http( error_url, session=session, show_secrets=show_secrets ) if token: return token print("[验证] 纯 HTTP 未拿到 captchaToken,停止重放;不启用浏览器") return "" def _print_weapon_proof_gap(raw: dict[str, Any]) -> bool: request_meta = raw.get("request_meta") if isinstance(raw.get("request_meta"), dict) else {} if not request_meta or ( bool(request_meta.get("has_kaw")) and bool(request_meta.get("has_kas")) ): return False print(" 当前请求缺少动态 kaw/kas 设备证明;这是与 APP 最终请求的已确认差异") return True def _print_login_failure_diagnosis( raw: dict[str, Any], *, passport_account_image: str ) -> None: body = raw.get("body") if isinstance(raw.get("body"), dict) else {} result = body.get("result") if result == 705: print("[诊断] result=705:触发验证/风控,不是 result=50 签名失败") _print_weapon_proof_gap(raw) if not passport_account_image: print(" 当前登录请求未带 passport_account_image") print(" 本次为纯在线新设备路径;705 多为号码风控触发交互式验证") print(" 自动验证码求解已启用,拿到 captchaToken 后会重放原请求") elif result == 50: print("[诊断] result=50:签名验证失败;继续对比 query/body/sig/__NS_sig3/__NS_xfalcon") def _print_session(session: Any) -> None: print(f" api_st = {_mask_secret(session.api_st)}") print(f" h5_st = {_mask_secret(session.h5_st)}") print(f" client_salt = {_mask_secret(session.client_salt)}") print(f" user_id = {session.user_id}") if session.region_ticket: print(f" region = {_mask_secret(session.region_ticket)}") print(f" mobile = {_mask_mobile(session.mobile)}") if session.pass_token: print(f" pass_token = {_mask_secret(session.pass_token)}") if session.quicklogin_token: print(f" quicklogin = {_mask_secret(session.quicklogin_token)}") def _account_cookie_fields(profile: Any, session: Any) -> dict[str, str]: fields = { str(key): "" if value is None else str(value) for key, value in login_api_params(profile).items() } fields.update(device_profile_cookie_fields(profile)) fields.update( { "userId": str(session.user_id), "ud": str(session.user_id), "client_key": "2ac2a76d", "kuaishou.api_st": str(session.api_st), "token": str(session.api_st), "region_ticket": str(session.region_ticket or ""), "__NSWJ": "", "kuaishou.h5_st": str(session.h5_st or ""), "os": "android", "cl": "", } ) return fields def _account_cookie_string(profile: Any, session: Any) -> str: fields = _account_cookie_fields(profile, session) emitted: set[str] = set() parts: list[str] = [] for key in _ACCOUNT_COOKIE_ORDER: if key in fields: parts.append(f"{key}={fields[key]}") emitted.add(key) for key in sorted(key for key in fields if key not in emitted): parts.append(f"{key}={fields[key]}") return "; ".join(parts) def _quote_env_value(value: str) -> str: return value.replace("\\", "\\\\").replace('"', '\\"') def _print_account_config(profile: Any, session: Any) -> None: cookie = _account_cookie_string(profile, session) value = f"账号#{cookie}#{session.client_salt}" print("\n[配置] 复制下面一行到 .env:") print(f'ksck="{_quote_env_value(value)}"') def _refresh_anonymous_region(profile: Any, session: Any) -> Any: region = None for region_host in (DEFAULT_REGION_HOST, REGION_FULL_CONFIG_HOST): region = refresh_region_ticket( profile, api_st="", client_salt="", user_id="", base_url=region_host, timeout=REQUEST_TIMEOUT_SECONDS, get_func=session.get, ) if region.ok: break if region_host != REGION_FULL_CONFIG_HOST: print( f"[区域] keyconfig 主机未命中 host={region_host} " f"status={region.status} error={region.error},尝试兼容主机" ) return region def main() -> int: args = build_parser().parse_args() # 设备身份只存在于本次进程;没有加载、种子复现或落盘路径。 profile = DeviceProfileGenerator().new_profile() print(f"[设备] 在线 DFP 注册中(did={profile.did})...") try: run_online_bootstrap(profile, timeout=REQUEST_TIMEOUT_SECONDS) print(f"[设备] 注册完成 did={profile.did} egid={profile.egid}") except Exception as exc: # noqa: BLE001 print(f"[设备] DFP 注册失败,改用本次本地画像继续: {exc}") passport_account_image = generate_profile_passport_account_image(profile) print( "[passport] 纯 Python 现场生成 " f"{passport_account_image[:14]}...{passport_account_image[-12:]} " f"(len={len(passport_account_image)})" ) weapon_header_provider = WeaponProofProvider(APK_DEFAULT_WEAPON_KAW) print( f"[Weapon] KAS 纯 Python 已启用 " f"kaw={_mask_secret(APK_DEFAULT_WEAPON_KAW, head=10, tail=8)} " f"(len={len(APK_DEFAULT_WEAPON_KAW)}, source=APK z_y_x_a fallback)" ) sig3_state = _new_sig3_state() seed = sig3_state.session_seed print( f"[sig3] 进程状态 seed=0x{seed:08x} " f"counter_start=0x{sig3_state.counter:08x}" ) base_url = args.base_url.rstrip("/") or None login_host = urllib.parse.urlparse(base_url or DEFAULT_LOGIN_HOST).hostname or "" http_session = create_http_session(args.transport) print(f"[传输] profile={args.transport}") region_ticket = "" anonymous_region = _refresh_anonymous_region(profile, http_session) if anonymous_region and anonymous_region.ok: region_ticket = anonymous_region.ticket _set_region_cookie(http_session, region_ticket, host=login_host) print( f"[区域] keyconfig 匿名票据已刷新 " f"uid={anonymous_region.uid or '0'} name={anonymous_region.name or '-'} " f"ticket={_mask_secret(region_ticket, head=12, tail=8)}" ) elif anonymous_region: print( f"[区域] keyconfig 匿名刷新未命中,继续无票据 " f"status={anonymous_region.status} error={anonymous_region.error}" ) code = args.code if not code: effective_base = base_url or DEFAULT_LOGIN_HOST print( f"[发码] -> {args.mobile} host={effective_base} " f"timeout={REQUEST_TIMEOUT_SECONDS}s type={LOGIN_SMS_CODE_TYPE}" ) send_encrypted_mobile = loginhelper_encrypt_mobile(args.mobile) send_captcha_token = "" send_retry = 0 while True: response = request_mobile_code( profile, args.mobile, mobile_country_code=LOGIN_COUNTRY_CODE, encrypted_mobile=send_encrypted_mobile, passport_account_image=passport_account_image, code_type=LOGIN_SMS_CODE_TYPE, need_check=False, prefetch_phone_number="", request_source="1", captcha_token=send_captcha_token, base_url=base_url, session_seed=seed, sig3_state=sig3_state, timeout=REQUEST_TIMEOUT_SECONDS, post_func=http_session.post, weapon_header_provider=weapon_header_provider, ) body = response.get("body") or {} print( f" status={response.get('status')} " f"body={_json_for_display(body, 300)}" ) if response.get("error"): print(f" error={response.get('error')}") print( "[发码] 请求未拿到响应;如果手机已收到短信," "重新执行并加 --code <验证码> 跳过发码继续登录" ) return 2 challenge_url = _captcha_error_url({"body": body}) if body.get("result") == 1 or not ( challenge_url and send_retry < CAPTCHA_RETRIES ): if body.get("result") != 1: print("[发码] 未返回 result=1(可能仍发了码 / 受风控);继续输入收到的验证码") break send_captcha_token = _complete_captcha_handoff( challenge_url, stage="requestMobileCode", session=http_session, ) if not send_captcha_token: return 4 send_retry += 1 print(f"[发码] captcha_token 已注入,重放发码 ({send_retry}/{CAPTCHA_RETRIES})") code = input("[输入] 收到的短信验证码: ").strip() print( f"[登录] mobileVerifyCode mobile={_mask_mobile(args.mobile)} " f"code= login_type={LOGIN_SMS_CODE_TYPE}" ) login_encrypted_mobile = loginhelper_encrypt_mobile(args.mobile) # 验证码重放必须复用同一组账号保护字段,保持原请求正文不变。 account_security_fields = build_account_security_fields() login_captcha_token = "" login_retry = 0 while True: login_session, results = login_by_code_try_paths( profile, args.mobile, code, mobile_country_code=LOGIN_COUNTRY_CODE, encrypted_mobile=login_encrypted_mobile, passport_account_image=passport_account_image, captcha_token=login_captcha_token, prefetch_phone_number=args.mobile, account_security_fields=account_security_fields, include_prefetch_phone_number=True, login_type=LOGIN_SMS_CODE_TYPE, base_url=base_url, session_seed=seed, sig3_state=sig3_state, timeout=REQUEST_TIMEOUT_SECONDS, post_func=http_session.post, weapon_header_provider=weapon_header_provider, ) challenge_url = "" for path, candidate_session in results: raw = ( candidate_session.raw if isinstance(candidate_session.raw, dict) else {} ) print(f"\n[{path}]") _print_raw_response(raw) _print_login_failure_diagnosis( raw, passport_account_image=passport_account_image ) challenge_url = challenge_url or _captcha_error_url(raw) if login_session.ok: break if not (challenge_url and login_retry < CAPTCHA_RETRIES): break login_captcha_token = _complete_captcha_handoff( challenge_url, stage="mobileVerifyCode", session=http_session, ) if not login_captcha_token: return 4 login_retry += 1 print(f"[登录] captcha_token 已注入,重放原接口 ({login_retry}/{CAPTCHA_RETRIES})") if not login_session.ok: print("\n[FAIL] mobileVerifyCode 未拿到会话(看上面 result/body)") return 1 http_session.cookies.set("kuaishou.api_st", login_session.api_st) http_session.cookies.set("token", login_session.api_st) if login_session.h5_st: http_session.cookies.set("kuaishou.h5_st", login_session.h5_st) if login_session.user_id: http_session.cookies.set("userId", login_session.user_id) http_session.cookies.set("ud", login_session.user_id) account_region = refresh_region_ticket( profile, api_st=login_session.api_st, client_salt=login_session.client_salt, user_id=login_session.user_id, base_url=DEFAULT_REGION_HOST, timeout=REQUEST_TIMEOUT_SECONDS, get_func=http_session.get, ) if account_region.ok: region_ticket = account_region.ticket _set_region_cookie(http_session, region_ticket, host=login_host) print( f"[区域] keyconfig 账号票据已刷新 " f"uid={account_region.uid or login_session.user_id} " f"name={account_region.name or '-'} " f"ticket={_mask_secret(region_ticket, head=12, tail=8)}" ) else: print( f"[区域] keyconfig 账号刷新未命中,保留匿名票据 " f"status={account_region.status} error={account_region.error}" ) login_session.region_ticket = region_ticket or login_session.region_ticket print("\n[OK] 登录成功,会话:") _print_session(login_session) _print_account_config(profile, login_session) return 0 def entrypoint() -> int: try: return main() except KeyboardInterrupt: print("\n[中断] 已停止;如果短信已收到,可用 --code <验证码> 继续登录") return 130 if __name__ == "__main__": raise SystemExit(entrypoint())