User-facing bugs resolved (per bug_reports table 2026-05-25): - 40cd28aa (short-scene filter): mobile api.ts default min_duration_sec=60 hides 6519 sub-60s scenes across all list endpoints (Performer/Site/Tag/ Browse). Caller may override with explicit 0. - 5e89ef7e (porndoe needs cookies/play click): INJECTED_JS in PlayerScreen now auto-clicks player-poster overlay (player-poster-play, big-play-button, vjs-big-play-button, jw-icon-display, btn-big-play, mejs__overlay-button, play-button, btn-play, videoPlayButton). Triggered same interval as consent-dismiss + ad-iframe removal. - b1b5e1a2 (Mixdrop czarny ekran): re-enable mixdrop direct stream via VPS curl_cffi proxy (was: skip → WebView fallback → blank screen). Backend pipeline (mixdrop.py extract + stream_proxy._curl_cffi_stream with JA3 + auto-refetch on token expire) was already complete; just removed the skip in app/api/playback.py. Plus ongoing WIP (paradisehill multi-part extraction, stream_proxy refetch logic, gesture race fix for long-press 2x speed, anti-adblock INJECTED_JS defenses, scripts for freshporno backfill, new sources API).
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""One-shot: parse APK Signing Block v2/v3 and print SHA-256(hex) of signing cert.
|
|
|
|
Matches what AntiTamperModule.kt computes — SHA-256 of the DER-encoded X.509 cert
|
|
that the PackageManager would return for the APK.
|
|
|
|
Spec: https://source.android.com/docs/security/features/apksigning/v2
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import struct
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
|
|
APK_SIG_BLOCK_MAGIC = b"APK Sig Block 42"
|
|
V2_BLOCK_ID = 0x7109871A
|
|
V3_BLOCK_ID = 0xF05368C0
|
|
V3_1_BLOCK_ID = 0x1B93AD61
|
|
|
|
|
|
def _find_eocd(data: bytes) -> int:
|
|
sig = b"PK\x05\x06"
|
|
# EOCD must be in last 65557 bytes
|
|
start = max(0, len(data) - 65557)
|
|
idx = data.rfind(sig, start)
|
|
if idx < 0:
|
|
raise RuntimeError("EOCD not found")
|
|
return idx
|
|
|
|
|
|
def _read_uint32_le(b: bytes, off: int) -> int:
|
|
return struct.unpack_from("<I", b, off)[0]
|
|
|
|
|
|
def _read_uint64_le(b: bytes, off: int) -> int:
|
|
return struct.unpack_from("<Q", b, off)[0]
|
|
|
|
|
|
def extract_sig_block(path: Path) -> bytes:
|
|
data = path.read_bytes()
|
|
eocd = _find_eocd(data)
|
|
cd_offset = _read_uint32_le(data, eocd + 16)
|
|
# Magic ends at cd_offset; the 8 bytes before magic are block size (excluding self)
|
|
magic_end = cd_offset
|
|
magic_start = magic_end - len(APK_SIG_BLOCK_MAGIC)
|
|
if data[magic_start:magic_end] != APK_SIG_BLOCK_MAGIC:
|
|
raise RuntimeError("APK Signing Block magic not found before central directory")
|
|
size_off = magic_start - 8
|
|
block_size_excl = _read_uint64_le(data, size_off)
|
|
# The block layout: size_of_block(8) | pairs | size_of_block(8) | magic(16)
|
|
block_total = block_size_excl + 8
|
|
block_start = magic_end - block_total
|
|
# Block = leading_size(8) | pairs | trailing_size(8) | magic(16)
|
|
# pairs region = between leading_size and trailing_size
|
|
return data[block_start + 8 : magic_start - 8]
|
|
|
|
|
|
def iter_pairs(pairs: bytes):
|
|
i = 0
|
|
n = len(pairs)
|
|
while i < n:
|
|
length = _read_uint64_le(pairs, i)
|
|
i += 8
|
|
pair_id = _read_uint32_le(pairs, i)
|
|
value = pairs[i + 4 : i + length]
|
|
yield pair_id, value
|
|
i += length
|
|
|
|
|
|
def extract_cert_der_v2_or_v3(block_value: bytes) -> bytes:
|
|
# block_value = "signers" sequence
|
|
# signers = length-prefixed sequence of signer
|
|
# signer = signed_data || signatures || public_key (each length-prefixed)
|
|
# signed_data = digests || certificates || additional_attributes (each length-prefixed)
|
|
# certificates = sequence of length-prefixed DER X.509 certs
|
|
|
|
def read_lp(buf: bytes, off: int) -> tuple[bytes, int]:
|
|
length = _read_uint32_le(buf, off)
|
|
return buf[off + 4 : off + 4 + length], off + 4 + length
|
|
|
|
# outer = signers sequence (already length-prefixed by caller? actually block_value IS the value)
|
|
# Per spec, the block "value" begins with sequence of length-prefixed signer structures
|
|
# i.e. no outer length prefix here.
|
|
off = 0
|
|
# The very first uint32 in block_value is the length of the signers sequence
|
|
signers_seq, _ = read_lp(block_value, 0)
|
|
off = 0
|
|
signer, _ = read_lp(signers_seq, off)
|
|
# signer = signed_data || min_sdk(4) || max_sdk(4) || signatures || public_key (v3 has sdk fields)
|
|
# Simplest: signed_data is the first length-prefixed blob in signer.
|
|
signed_data, _ = read_lp(signer, 0)
|
|
# signed_data = digests || certificates || ...
|
|
inner_off = 0
|
|
digests, inner_off = read_lp(signed_data, inner_off)
|
|
certs_seq, inner_off = read_lp(signed_data, inner_off)
|
|
# certs_seq = sequence of length-prefixed DER certs
|
|
first_cert, _ = read_lp(certs_seq, 0)
|
|
return first_cert
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if len(argv) < 2:
|
|
print("usage: _extract_apk_sig_hash.py <path-to-apk>", file=sys.stderr)
|
|
return 2
|
|
apk = Path(argv[1])
|
|
if not apk.is_file():
|
|
print(f"not found: {apk}", file=sys.stderr)
|
|
return 2
|
|
|
|
pairs = extract_sig_block(apk)
|
|
found_block: bytes | None = None
|
|
chosen_id = None
|
|
for pid, value in iter_pairs(pairs):
|
|
if pid in (V2_BLOCK_ID, V3_BLOCK_ID, V3_1_BLOCK_ID):
|
|
# prefer v3 over v2 if both present (matches what PM returns on modern Android)
|
|
if chosen_id in (None, V2_BLOCK_ID) and pid in (V3_BLOCK_ID, V3_1_BLOCK_ID):
|
|
found_block = value
|
|
chosen_id = pid
|
|
elif chosen_id is None:
|
|
found_block = value
|
|
chosen_id = pid
|
|
if found_block is None:
|
|
print("no v2/v3 signing block found", file=sys.stderr)
|
|
return 3
|
|
|
|
cert_der = extract_cert_der_v2_or_v3(found_block)
|
|
sha = hashlib.sha256(cert_der).hexdigest()
|
|
print(sha)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|