175 lines
5.1 KiB
Python
175 lines
5.1 KiB
Python
"""DFP / unifiedId atlasSign input builders and 10405 sign wrappers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
from typing import Mapping
|
|
|
|
from .atlas_sign import atlas_sign, atlas_sign_to_digest_hex
|
|
from .enc_data import ZT_OUTER_CONFIGS
|
|
from .sig3 import kwsg_10418_digest24_unmix
|
|
|
|
|
|
DFP_SDK_ID = "7e46b28a-8c93-4940-8238-4c60e64e3c81"
|
|
DFP_SIGN_KEYS = {"sign", "__NS_sig3", "__NS_xfalcon", "__NStokensig", "sig"}
|
|
DFP_PAYLOAD_KEYS = ("deviceInfo", "carryInfo", "data")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DfpSignMaterial:
|
|
scheme: str
|
|
keys: tuple[str, ...]
|
|
sign_input: str
|
|
excluded_keys: tuple[str, ...] = ()
|
|
note: str = ""
|
|
|
|
@property
|
|
def input_len(self) -> int:
|
|
return len(self.sign_input)
|
|
|
|
@property
|
|
def input_sha256(self) -> str:
|
|
return hashlib.sha256(self.sign_input.encode("utf-8", errors="ignore")).hexdigest()
|
|
|
|
|
|
def _first_payload_key(params: Mapping[str, str], preferred: tuple[str, ...]) -> str:
|
|
for key in preferred:
|
|
if key in params:
|
|
return key
|
|
for key in DFP_PAYLOAD_KEYS:
|
|
if key in params:
|
|
return key
|
|
raise KeyError("missing DFP encrypted payload field")
|
|
|
|
|
|
def legacy_product_ts_sv_payload(params: Mapping[str, str]) -> DfpSignMaterial:
|
|
"""Build `productName + ts + sv + encryptedPayload`.
|
|
|
|
Used by `gdfp/report` and `unifiedId/logReport/android`.
|
|
"""
|
|
|
|
payload_key = _first_payload_key(params, ("deviceInfo", "carryInfo", "data"))
|
|
keys = ("productName", "ts", "sv", payload_key)
|
|
sign_input = (
|
|
params.get("productName", "")
|
|
+ params.get("ts", "")
|
|
+ params.get("sv", "2")
|
|
+ params.get(payload_key, "")
|
|
)
|
|
excluded = tuple(key for key in params if key not in keys and key not in DFP_SIGN_KEYS)
|
|
return DfpSignMaterial(
|
|
scheme="legacy_product_ts_sv_payload",
|
|
keys=keys,
|
|
sign_input=sign_input,
|
|
excluded_keys=excluded,
|
|
note="productName + ts + sv + encrypted payload; append rdid/didtag/ft after sign",
|
|
)
|
|
|
|
|
|
def id_mapping_data_only(params: Mapping[str, str]) -> DfpSignMaterial:
|
|
"""Build `data` only for `unifiedId/logReport/idMapping`."""
|
|
|
|
payload_key = _first_payload_key(params, ("data",))
|
|
return DfpSignMaterial(
|
|
scheme="id_mapping_data_only",
|
|
keys=(payload_key,),
|
|
sign_input=params.get(payload_key, ""),
|
|
note="idMapping signs only encoded data",
|
|
)
|
|
|
|
|
|
def tree_values_sorted_non_empty_except_sign(params: Mapping[str, str]) -> DfpSignMaterial:
|
|
"""Build TreeMap-style non-empty values without separators."""
|
|
|
|
keys = tuple(
|
|
key
|
|
for key in sorted(params)
|
|
if key not in DFP_SIGN_KEYS and params.get(key, "") != ""
|
|
)
|
|
sign_input = "".join(params[key] for key in keys)
|
|
excluded = tuple(key for key in params if key not in keys and key not in DFP_SIGN_KEYS)
|
|
return DfpSignMaterial(
|
|
scheme="tree_values_sorted_non_empty_except_sign",
|
|
keys=keys,
|
|
sign_input=sign_input,
|
|
excluded_keys=excluded,
|
|
note="TreeMap key order, non-empty values, no separators",
|
|
)
|
|
|
|
|
|
def build_dfp_sign_material(kind: str, params: Mapping[str, str]) -> DfpSignMaterial:
|
|
if kind in {"gdfp_report", "unified_log_report"}:
|
|
return legacy_product_ts_sv_payload(params)
|
|
if kind == "unified_id_mapping":
|
|
return id_mapping_data_only(params)
|
|
if kind in {"unified_fetch", "unified_repair", "unified_check_repair"}:
|
|
return tree_values_sorted_non_empty_except_sign(params)
|
|
raise ValueError(f"unsupported DFP sign kind: {kind}")
|
|
|
|
|
|
def dfp_atlas_sign(
|
|
sign_input: str | bytes | bytearray,
|
|
counter: int,
|
|
unix_time: int,
|
|
*,
|
|
session_seed: int,
|
|
sdk_id: str = DFP_SDK_ID,
|
|
) -> str:
|
|
"""Generate the 64hex DFP 10405 atlasSign value."""
|
|
|
|
return atlas_sign(
|
|
sign_input,
|
|
sdk_id,
|
|
counter,
|
|
unix_time,
|
|
session_seed=session_seed,
|
|
)
|
|
|
|
|
|
def sign_dfp_form(
|
|
kind: str,
|
|
params: Mapping[str, str],
|
|
counter: int,
|
|
unix_time: int,
|
|
*,
|
|
session_seed: int,
|
|
sdk_id: str = DFP_SDK_ID,
|
|
) -> dict[str, str]:
|
|
"""Return a signed copy of a DFP/unifiedId form."""
|
|
|
|
material = build_dfp_sign_material(kind, params)
|
|
signed = {str(key): str(value) for key, value in params.items()}
|
|
signed["sign"] = dfp_atlas_sign(
|
|
material.sign_input,
|
|
counter,
|
|
unix_time,
|
|
session_seed=session_seed,
|
|
sdk_id=sdk_id,
|
|
)
|
|
return signed
|
|
|
|
|
|
def parse_dfp_atlas_sign(sign_hex64: str, sdk_id: str = DFP_SDK_ID) -> dict:
|
|
digest_hex = atlas_sign_to_digest_hex(sign_hex64, sdk_id)
|
|
parsed = kwsg_10418_digest24_unmix(digest_hex)
|
|
parsed["digest24_hex"] = digest_hex
|
|
parsed["sdk_id"] = sdk_id
|
|
parsed["head8"] = ZT_OUTER_CONFIGS[sdk_id]["head8"].hex()
|
|
return parsed
|
|
|
|
|
|
__all__ = [
|
|
"DFP_PAYLOAD_KEYS",
|
|
"DFP_SDK_ID",
|
|
"DFP_SIGN_KEYS",
|
|
"DfpSignMaterial",
|
|
"build_dfp_sign_material",
|
|
"dfp_atlas_sign",
|
|
"id_mapping_data_only",
|
|
"legacy_product_ts_sv_payload",
|
|
"parse_dfp_atlas_sign",
|
|
"sign_dfp_form",
|
|
"tree_values_sorted_non_empty_except_sign",
|
|
]
|