1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt, ' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety; zakresy 0–5 i A–Z na dywiz. Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach, czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu, tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana. Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie). Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika to zero zmiany. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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))
|