goon/scripts/_extract_apk_sig_hash.py
https://github.com/goon-foss/goon 7979d5fa61 session work: bug-report fixes + WIP cleanup
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).
2026-05-25 22:02:52 +02:00

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))