diff --git a/fdroidserver/apksigcopier.py b/fdroidserver/apksigcopier.py index 321aec52..2ab0b8c4 100644 --- a/fdroidserver/apksigcopier.py +++ b/fdroidserver/apksigcopier.py @@ -1,26 +1,29 @@ #!/usr/bin/python3 # encoding: utf-8 -# SPDX-FileCopyrightText: 2022 FC Stegerman +# SPDX-FileCopyrightText: 2023 FC Stegerman # SPDX-License-Identifier: GPL-3.0-or-later # -- ; {{{1 # # File : apksigcopier # Maintainer : FC Stegerman -# Date : 2022-11-01 +# Date : 2023-02-08 # -# Copyright : Copyright (C) 2022 FC Stegerman -# Version : v1.1.0 +# Copyright : Copyright (C) 2023 FC Stegerman +# Version : v1.1.1 # License : GPLv3+ # # -- ; }}}1 """ -Copy/extract/patch android apk signatures. +Copy/extract/patch android apk signatures & compare apks. apksigcopier is a tool for copying android APK signatures from a signed APK to an unsigned one (in order to verify reproducible builds). +It can also be used to compare two APKs with different signatures; this requires +apksigner. + CLI === @@ -28,27 +31,33 @@ CLI $ apksigcopier extract [OPTIONS] SIGNED_APK OUTPUT_DIR $ apksigcopier patch [OPTIONS] METADATA_DIR UNSIGNED_APK OUTPUT_APK $ apksigcopier copy [OPTIONS] SIGNED_APK UNSIGNED_APK OUTPUT_APK +$ apksigcopier compare [OPTIONS] FIRST_APK SECOND_APK The following environment variables can be set to 1, yes, or true to override the default behaviour: +* set APKSIGCOPIER_EXCLUDE_ALL_META=1 to exclude all metadata files * set APKSIGCOPIER_COPY_EXTRA_BYTES=1 to copy extra bytes after data (e.g. a v2 sig) +* set APKSIGCOPIER_SKIP_REALIGNMENT=1 to skip realignment of ZIP entries API === ->> from apksigcopier import do_extract, do_patch, do_copy +>> from apksigcopier import do_extract, do_patch, do_copy, do_compare >> do_extract(signed_apk, output_dir, v1_only=NO) >> do_patch(metadata_dir, unsigned_apk, output_apk, v1_only=NO) >> do_copy(signed_apk, unsigned_apk, output_apk, v1_only=NO) +>> do_compare(first_apk, second_apk, unsigned=False) You can use False, None, and True instead of NO, AUTO, and YES respectively. The following global variables (which default to False), can be set to override the default behaviour: +* set exclude_all_meta=True to exclude all metadata files * set copy_extra_bytes=True to copy extra bytes after data (e.g. a v2 sig) +* set skip_realignment=True to skip realignment of ZIP entries """ import glob @@ -56,17 +65,23 @@ import json import os import re import struct +import sys import zipfile import zlib from collections import namedtuple -from typing import Any, BinaryIO, Dict, Iterable, Iterator, Literal, Optional, Tuple, Union +from typing import Any, BinaryIO, Callable, Dict, Iterable, Iterator, Optional, Tuple, Union -__version__ = "1.1.0" +__version__ = "1.1.1" NAME = "apksigcopier" +if sys.version_info >= (3, 8): + from typing import Literal + NoAutoYes = Literal["no", "auto", "yes"] +else: + NoAutoYes = str + DateTime = Tuple[int, int, int, int, int, int] -NoAutoYes = Literal["no", "auto", "yes"] NoAutoYesBoolNone = Union[NoAutoYes, bool, None] ZipInfoDataPairs = Iterable[Tuple[zipfile.ZipInfo, bytes]] @@ -75,6 +90,7 @@ NOAUTOYES: Tuple[NoAutoYes, NoAutoYes, NoAutoYes] = ("no", "auto", "yes") NO, AUTO, YES = NOAUTOYES APK_META = re.compile(r"^META-INF/([0-9A-Za-z_-]+\.(SF|RSA|DSA|EC)|MANIFEST\.MF)$") META_EXT: Tuple[str, ...] = ("SF", "RSA|DSA|EC", "MF") +COPY_EXCLUDE: Tuple[str, ...] = ("META-INF/MANIFEST.MF",) DATETIMEZERO: DateTime = (1980, 0, 0, 0, 0, 0) ################################################################################ @@ -107,7 +123,9 @@ VALID_ZIP_META = dict( ZipData = namedtuple("ZipData", ("cd_offset", "eocd_offset", "cd_and_eocd")) +exclude_all_meta = False # exclude all metadata files in copy_apk() copy_extra_bytes = False # copy extra bytes after data in copy_apk() +skip_realignment = False # skip realignment of ZIP entries in copy_apk() class APKSigCopierError(Exception): @@ -132,14 +150,15 @@ class ReproducibleZipInfo(zipfile.ZipInfo): _override: Dict[str, Any] = {} - def __init__(self, zinfo, **override): # pylint: disable=W0231 + def __init__(self, zinfo: zipfile.ZipInfo, **override: Any) -> None: + # pylint: disable=W0231 if override: self._override = {**self._override, **override} for k in self.__slots__: if hasattr(zinfo, k): setattr(self, k, getattr(zinfo, k)) - def __getattribute__(self, name): + def __getattribute__(self, name: str) -> Any: if name != "_override": try: return self._override[name] @@ -192,10 +211,23 @@ def is_meta(filename: str) -> bool: """ Check whether filename is a JAR metadata file. - This is true when filename is a v1 (JAR) signature file (.SF), signature - block file (.RSA, .DSA, or .EC), or manifest (MANIFEST.MF). + Returns whether filename is a v1 (JAR) signature file (.SF), signature block + file (.RSA, .DSA, or .EC), or manifest (MANIFEST.MF). See https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html + + >>> from apksigcopier import is_meta + >>> is_meta("classes.dex") + False + >>> is_meta("META-INF/CERT.SF") + True + >>> is_meta("META-INF/CERT.RSA") + True + >>> is_meta("META-INF/MANIFEST.MF") + True + >>> is_meta("META-INF/OOPS") + False + """ return APK_META.fullmatch(filename) is not None @@ -204,10 +236,60 @@ def exclude_from_copying(filename: str) -> bool: """ Check whether to exclude a file during copy_apk(). - Fdroidserver always wants JAR Signature files to be excluded, so - it excludes all metadata files as matched by is_meta(). + Excludes filenames in COPY_EXCLUDE (i.e. MANIFEST.MF) by default; when + exclude_all_meta is set to True instead, excludes all metadata files as + matched by is_meta(). + + Directories are always excluded. + + >>> import apksigcopier + >>> from apksigcopier import exclude_from_copying + >>> exclude_from_copying("classes.dex") + False + >>> exclude_from_copying("foo/") + True + >>> exclude_from_copying("META-INF/") + True + >>> exclude_from_copying("META-INF/MANIFEST.MF") + True + >>> exclude_from_copying("META-INF/CERT.SF") + False + >>> exclude_from_copying("META-INF/OOPS") + False + + >>> apksigcopier.exclude_all_meta = True + >>> exclude_from_copying("classes.dex") + False + >>> exclude_from_copying("META-INF/") + True + >>> exclude_from_copying("META-INF/MANIFEST.MF") + True + >>> exclude_from_copying("META-INF/CERT.SF") + True + >>> exclude_from_copying("META-INF/OOPS") + False + """ - return is_meta(filename) + return exclude_meta(filename) if exclude_all_meta else exclude_default(filename) + + +def exclude_default(filename: str) -> bool: + """ + Like exclude_from_copying(). + + Excludes directories and filenames in COPY_EXCLUDE (i.e. MANIFEST.MF). + """ + return is_directory(filename) or filename in COPY_EXCLUDE + + +def exclude_meta(filename: str) -> bool: + """Like exclude_from_copying(); excludes directories and all metadata files.""" + return is_directory(filename) or is_meta(filename) + + +def is_directory(filename: str) -> bool: + """ZIP entries with filenames that end with a '/' are directories.""" + return filename.endswith("/") ################################################################################ @@ -247,6 +329,27 @@ def zipflinger_virtual_entry(size: int) -> bytes: ) + int.to_bytes(size - 30, 2, "little") + b"\x00" * (size - 30) +def detect_zfe(apkfile: str) -> Optional[int]: + """ + Detect zipflinger virtual entry. + + Returns the size of the virtual entry if found, None otherwise. + + Raises ZipError if the size is less than 30 or greater than 4096, or the + data isn't all zeroes. + """ + with open(apkfile, "rb") as fh: + zfe_start = zipflinger_virtual_entry(30)[:28] # w/o len(extra) + if fh.read(28) == zfe_start: + zfe_size = 30 + int.from_bytes(fh.read(2), "little") + if not (30 <= zfe_size <= 4096): + raise ZipError("Unsupported virtual entry size") + if not fh.read(zfe_size - 30) == b"\x00" * (zfe_size - 30): + raise ZipError("Unsupported virtual entry data") + return zfe_size + return None + + ################################################################################ # # https://en.wikipedia.org/wiki/ZIP_(file_format) @@ -282,22 +385,73 @@ def zipflinger_virtual_entry(size: int) -> bytes: # FIXME: handle utf8 filenames w/o utf8 flag (as produced by zipflinger)? # https://android.googlesource.com/platform/tools/apksig # src/main/java/com/android/apksig/ApkSigner.java -def copy_apk(unsigned_apk: str, output_apk: str, *, zfe_size: Optional[int] = None) -> DateTime: +def copy_apk(unsigned_apk: str, output_apk: str, *, + copy_extra: Optional[bool] = None, + exclude: Optional[Callable[[str], bool]] = None, + realign: Optional[bool] = None, + zfe_size: Optional[int] = None) -> DateTime: """ Copy APK like apksigner would, excluding files matched by exclude_from_copying(). Adds a zipflinger virtual entry of zfe_size bytes if one is not already present and zfe_size is not None. + Returns max date_time. + The following global variables (which default to False), can be set to override the default behaviour: + * set exclude_all_meta=True to exclude all metadata files * set copy_extra_bytes=True to copy extra bytes after data (e.g. a v2 sig) + * set skip_realignment=True to skip realignment of ZIP entries + + The default behaviour can also be changed using the keyword-only arguments + exclude, copy_extra, and realign; these take precedence over the global + variables when not None. NB: exclude is a callable, not a bool; realign is + the inverse of skip_realignment. + + >>> import apksigcopier, os, zipfile + >>> apk = "test/apks/apks/golden-aligned-in.apk" + >>> with zipfile.ZipFile(apk, "r") as zf: + ... infos_in = zf.infolist() + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... out = os.path.join(tmpdir, "out.apk") + ... apksigcopier.copy_apk(apk, out) + ... with zipfile.ZipFile(out, "r") as zf: + ... infos_out = zf.infolist() + (2017, 5, 15, 11, 28, 40) + >>> for i in infos_in: + ... print(i.filename) + META-INF/ + META-INF/MANIFEST.MF + AndroidManifest.xml + classes.dex + temp.txt + lib/armeabi/fake.so + resources.arsc + temp2.txt + >>> for i in infos_out: + ... print(i.filename) + AndroidManifest.xml + classes.dex + temp.txt + lib/armeabi/fake.so + resources.arsc + temp2.txt + >>> infos_in[2] + + >>> infos_out[0] + + >>> repr(infos_in[2:]) == repr(infos_out) + True - Returns - ------- - max date_time """ + if copy_extra is None: + copy_extra = copy_extra_bytes + if exclude is None: + exclude = exclude_from_copying + if realign is None: + realign = not skip_realignment with zipfile.ZipFile(unsigned_apk, "r") as zf: infos = zf.infolist() zdata = zip_data(unsigned_apk) @@ -318,15 +472,16 @@ def copy_apk(unsigned_apk: str, output_apk: str, *, zfe_size: Optional[int] = No raise ZipError("Expected local file header signature") n, m = struct.unpack(" bytes: +def _realign_zip_entry(info: zipfile.ZipInfo, hdr: bytes, n: int, m: int, + off_o: int, pad_like_apksigner: bool = True) -> bytes: align = 4096 if info.filename.endswith(".so") else 4 old_off = 30 + n + m + info.header_offset new_off = 30 + n + m + off_o - old_xtr = info.extra + old_xtr = hdr[30 + n:30 + n + m] new_xtr = b"" while len(old_xtr) >= 4: hdr_id, size = struct.unpack(" Iterator[Tuple[zipfile.ZipInfo, bytes]]: Extract v1 signature metadata files from signed APK. Yields (ZipInfo, data) pairs. + + >>> from apksigcopier import extract_meta + >>> apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" + >>> meta = tuple(extract_meta(apk)) + >>> [ x.filename for x, _ in meta ] + ['META-INF/RSA-2048.SF', 'META-INF/RSA-2048.RSA', 'META-INF/MANIFEST.MF'] + >>> for line in meta[0][1].splitlines()[:4]: + ... print(line.decode()) + Signature-Version: 1.0 + Created-By: 1.0 (Android) + SHA-256-Digest-Manifest: hz7AxDJU9Namxoou/kc4Z2GVRS9anCGI+M52tbCsXT0= + X-Android-APK-Signed: 2, 3 + >>> for line in meta[2][1].splitlines()[:2]: + ... print(line.decode()) + Manifest-Version: 1.0 + Created-By: 1.8.0_45-internal (Oracle Corporation) + """ with zipfile.ZipFile(signed_apk, "r") as zf_sig: for info in zf_sig.infolist(): @@ -410,34 +587,62 @@ def extract_meta(signed_apk: str) -> Iterator[Tuple[zipfile.ZipInfo, bytes]]: def extract_differences(signed_apk: str, extracted_meta: ZipInfoDataPairs) \ -> Optional[Dict[str, Any]]: - """Extract ZIP metadata differences from signed APK.""" + """ + Extract ZIP metadata differences from signed APK. + + >>> import apksigcopier as asc, pprint + >>> apk = "test/apks/apks/debuggable-boolean.apk" + >>> meta = tuple(asc.extract_meta(apk)) + >>> [ x.filename for x, _ in meta ] + ['META-INF/CERT.SF', 'META-INF/CERT.RSA', 'META-INF/MANIFEST.MF'] + >>> diff = asc.extract_differences(apk, meta) + >>> pprint.pprint(diff) + {'files': {'META-INF/CERT.RSA': {'flag_bits': 2056}, + 'META-INF/CERT.SF': {'flag_bits': 2056}, + 'META-INF/MANIFEST.MF': {'flag_bits': 2056}}} + + >>> meta[2][0].extract_version = 42 + >>> try: + ... asc.extract_differences(apk, meta) + ... except asc.ZipError as e: + ... print(e) + Unsupported extract_version + + >>> asc.validate_differences(diff) is None + True + >>> diff["files"]["META-INF/OOPS"] = {} + >>> asc.validate_differences(diff) + ".files key 'META-INF/OOPS' is not a metadata file" + >>> del diff["files"]["META-INF/OOPS"] + >>> diff["files"]["META-INF/CERT.RSA"]["compresslevel"] = 42 + >>> asc.validate_differences(diff) + ".files['META-INF/CERT.RSA'].compresslevel has an unexpected value" + >>> diff["oops"] = 42 + >>> asc.validate_differences(diff) + 'contains unknown key(s)' + + """ differences: Dict[str, Any] = {} files = {} for info, data in extracted_meta: diffs = {} - for k in VALID_ZIP_META.keys(): + for k in VALID_ZIP_META: if k != "compresslevel": v = getattr(info, k) if v != APKZipInfo._override[k]: if v not in VALID_ZIP_META[k]: raise ZipError(f"Unsupported {k}") diffs[k] = v - level = _get_compresslevel(info, data) + level = _get_compresslevel(signed_apk, info, data) if level != APKZipInfo.COMPRESSLEVEL: diffs["compresslevel"] = level if diffs: files[info.filename] = diffs if files: differences["files"] = files - with open(signed_apk, "rb") as fh: - zfe_start = zipflinger_virtual_entry(30)[:28] # w/o len(extra) - if fh.read(28) == zfe_start: - zfe_size = 30 + int.from_bytes(fh.read(2), "little") - if not (30 <= zfe_size <= 4096): - raise ZipError("Unsupported virtual entry size") - if not fh.read(zfe_size - 30) == b"\x00" * (zfe_size - 30): - raise ZipError("Unsupported virtual entry data") - differences["zipflinger_virtual_entry"] = zfe_size + zfe_size = detect_zfe(signed_apk) + if zfe_size: + differences["zipflinger_virtual_entry"] = zfe_size return differences or None @@ -447,7 +652,7 @@ def validate_differences(differences: Dict[str, Any]) -> Optional[str]: Returns None if valid, error otherwise. """ - if set(differences.keys()) - {"files", "zipflinger_virtual_entry"}: + if set(differences) - {"files", "zipflinger_virtual_entry"}: return "contains unknown key(s)" if "zipflinger_virtual_entry" in differences: if type(differences["zipflinger_virtual_entry"]) is not int: @@ -458,9 +663,11 @@ def validate_differences(differences: Dict[str, Any]) -> Optional[str]: if not isinstance(differences["files"], dict): return ".files is not a dict" for name, info in differences["files"].items(): + if not is_meta(name): + return f".files key {name!r} is not a metadata file" if not isinstance(info, dict): return f".files[{name!r}] is not a dict" - if set(info.keys()) - set(VALID_ZIP_META.keys()): + if set(info) - set(VALID_ZIP_META): return f".files[{name!r}] contains unknown key(s)" for k, v in info.items(): if v not in VALID_ZIP_META[k]: @@ -468,21 +675,72 @@ def validate_differences(differences: Dict[str, Any]) -> Optional[str]: return None -# FIXME: false positives on same compressed size? compare actual data? -def _get_compresslevel(info: zipfile.ZipInfo, data: bytes) -> int: +def _get_compresslevel(apkfile: str, info: zipfile.ZipInfo, data: bytes) -> int: if info.compress_type != 8: raise ZipError("Unsupported compress_type") + crc = _get_compressed_crc(apkfile, info) for level in VALID_ZIP_META["compresslevel"]: comp = zlib.compressobj(level, 8, -15) - if len(comp.compress(data) + comp.flush()) == info.compress_size: + if zlib.crc32(comp.compress(data) + comp.flush()) == crc: return level raise ZipError("Unsupported compresslevel") +def _get_compressed_crc(apkfile: str, info: zipfile.ZipInfo) -> int: + with open(apkfile, "rb") as fh: + fh.seek(info.header_offset) + hdr = fh.read(30) + if hdr[:4] != b"\x50\x4b\x03\x04": + raise ZipError("Expected local file header signature") + n, m = struct.unpack(" None: - """Add v1 signature metadata to APK (removes v2 sig block, if any).""" + """ + Add v1 signature metadata to APK (removes v2 sig block, if any). + + >>> import apksigcopier as asc + >>> unsigned_apk = "test/apks/apks/golden-aligned-in.apk" + >>> signed_apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" + >>> meta = tuple(asc.extract_meta(signed_apk)) + >>> [ x.filename for x, _ in meta ] + ['META-INF/RSA-2048.SF', 'META-INF/RSA-2048.RSA', 'META-INF/MANIFEST.MF'] + >>> with zipfile.ZipFile(unsigned_apk, "r") as zf: + ... infos_in = zf.infolist() + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... out = os.path.join(tmpdir, "out.apk") + ... asc.copy_apk(unsigned_apk, out) + ... asc.patch_meta(meta, out) + ... with zipfile.ZipFile(out, "r") as zf: + ... infos_out = zf.infolist() + (2017, 5, 15, 11, 28, 40) + >>> for i in infos_in: + ... print(i.filename) + META-INF/ + META-INF/MANIFEST.MF + AndroidManifest.xml + classes.dex + temp.txt + lib/armeabi/fake.so + resources.arsc + temp2.txt + >>> for i in infos_out: + ... print(i.filename) + AndroidManifest.xml + classes.dex + temp.txt + lib/armeabi/fake.so + resources.arsc + temp2.txt + META-INF/RSA-2048.SF + META-INF/RSA-2048.RSA + META-INF/MANIFEST.MF + + """ with zipfile.ZipFile(output_apk, "r") as zf_out: for info in zf_out.infolist(): if is_meta(info.filename): @@ -504,6 +762,22 @@ def extract_v2_sig(apkfile: str, expected: bool = True) -> Optional[Tuple[int, b When successful, returns (sb_offset, sig_block); otherwise raises NoAPKSigningBlock when expected is True, else returns None. + + >>> import apksigcopier as asc + >>> apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" + >>> sb_offset, sig_block = asc.extract_v2_sig(apk) + >>> sb_offset + 8192 + >>> len(sig_block) + 4096 + + >>> apk = "test/apks/apks/golden-aligned-in.apk" + >>> try: + ... asc.extract_v2_sig(apk) + ... except asc.NoAPKSigningBlock as e: + ... print(e) + No APK Signing Block + """ cd_offset = zip_data(apkfile).cd_offset with open(apkfile, "rb") as fh: @@ -529,9 +803,16 @@ def zip_data(apkfile: str, count: int = 1024) -> ZipData: """ Extract central directory, EOCD, and offsets from ZIP. - Returns - ------- - ZipData + Returns ZipData. + + >>> import apksigcopier + >>> apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" + >>> data = apksigcopier.zip_data(apk) + >>> data.cd_offset, data.eocd_offset + (12288, 12843) + >>> len(data.cd_and_eocd) + 577 + """ with open(apkfile, "rb") as fh: fh.seek(-count, os.SEEK_END) @@ -550,7 +831,28 @@ def zip_data(apkfile: str, count: int = 1024) -> ZipData: # FIXME: can we determine signed_sb_offset? def patch_v2_sig(extracted_v2_sig: Tuple[int, bytes], output_apk: str) -> None: - """Implant extracted v2/v3 signature into APK.""" + """ + Implant extracted v2/v3 signature into APK. + + >>> import apksigcopier as asc + >>> unsigned_apk = "test/apks/apks/golden-aligned-in.apk" + >>> signed_apk = "test/apks/apks/golden-aligned-v1v2v3-out.apk" + >>> meta = tuple(asc.extract_meta(signed_apk)) + >>> v2_sig = asc.extract_v2_sig(signed_apk) + >>> with tempfile.TemporaryDirectory() as tmpdir: + ... out = os.path.join(tmpdir, "out.apk") + ... date_time = asc.copy_apk(unsigned_apk, out) + ... asc.patch_meta(meta, out, date_time=date_time) + ... asc.extract_v2_sig(out, expected=False) is None + ... asc.patch_v2_sig(v2_sig, out) + ... asc.extract_v2_sig(out) == v2_sig + ... with open(signed_apk, "rb") as a, open(out, "rb") as b: + ... a.read() == b.read() + True + True + True + + """ signed_sb_offset, signed_sb = extracted_v2_sig data_out = zip_data(output_apk) if signed_sb_offset < data_out.cd_offset: @@ -568,13 +870,14 @@ def patch_v2_sig(extracted_v2_sig: Tuple[int, bytes], output_apk: str) -> None: def patch_apk(extracted_meta: ZipInfoDataPairs, extracted_v2_sig: Optional[Tuple[int, bytes]], unsigned_apk: str, output_apk: str, *, - differences: Optional[Dict[str, Any]] = None) -> None: + differences: Optional[Dict[str, Any]] = None, + exclude: Optional[Callable[[str], bool]] = None) -> None: """Patch extracted_meta + extracted_v2_sig (if not None) onto unsigned_apk and save as output_apk.""" if differences and "zipflinger_virtual_entry" in differences: zfe_size = differences["zipflinger_virtual_entry"] else: zfe_size = None - date_time = copy_apk(unsigned_apk, output_apk, zfe_size=zfe_size) + date_time = copy_apk(unsigned_apk, output_apk, exclude=exclude, zfe_size=zfe_size) patch_meta(extracted_meta, output_apk, date_time=date_time, differences=differences) if extracted_v2_sig is not None: patch_v2_sig(extracted_v2_sig, output_apk) @@ -625,7 +928,9 @@ def do_extract(signed_apk: str, output_dir: str, v1_only: NoAutoYesBoolNone = NO # FIXME: support multiple signers? def do_patch(metadata_dir: str, unsigned_apk: str, output_apk: str, - v1_only: NoAutoYesBoolNone = NO, *, ignore_differences: bool = False) -> None: + v1_only: NoAutoYesBoolNone = NO, *, + exclude: Optional[Callable[[str], bool]] = None, + ignore_differences: bool = False) -> None: """ Patch signatures from metadata_dir onto unsigned_apk and save as output_apk. @@ -674,11 +979,13 @@ def do_patch(metadata_dir: str, unsigned_apk: str, output_apk: str, if not extracted_meta and extracted_v2_sig is None: raise APKSigCopierError("Expected v1 and/or v2/v3 signature, found neither") patch_apk(extracted_meta, extracted_v2_sig, unsigned_apk, output_apk, - differences=differences) + differences=differences, exclude=exclude) def do_copy(signed_apk: str, unsigned_apk: str, output_apk: str, - v1_only: NoAutoYesBoolNone = NO, *, ignore_differences: bool = False) -> None: + v1_only: NoAutoYesBoolNone = NO, *, + exclude: Optional[Callable[[str], bool]] = None, + ignore_differences: bool = False) -> None: """ Copy signatures from signed_apk onto unsigned_apk and save as output_apk. @@ -698,6 +1005,6 @@ def do_copy(signed_apk: str, unsigned_apk: str, output_apk: str, if extracted_v2_sig is not None and not ignore_differences: differences = extract_differences(signed_apk, extracted_meta) patch_apk(extracted_meta, extracted_v2_sig, unsigned_apk, output_apk, - differences=differences) + differences=differences, exclude=exclude) # vim: set tw=80 sw=4 sts=4 et fdm=marker : diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 19f5f9e5..1354fd1f 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -3258,7 +3258,8 @@ def apk_implant_signatures(apkpath, outpath, manifest): """ sigdir = os.path.dirname(manifest) # FIXME - apksigcopier.do_patch(sigdir, apkpath, outpath, v1_only=None) + apksigcopier.do_patch(sigdir, apkpath, outpath, v1_only=None, + exclude=apksigcopier.exclude_meta) def apk_extract_signatures(apkpath, outdir): @@ -3388,7 +3389,8 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir, v1_only=None): tmp_apk = os.path.join(tmp_dir, 'sigcp_' + os.path.basename(unsigned_apk)) try: - apksigcopier.do_copy(signed_apk, unsigned_apk, tmp_apk, v1_only=v1_only) + apksigcopier.do_copy(signed_apk, unsigned_apk, tmp_apk, v1_only=v1_only, + exclude=apksigcopier.exclude_meta) except apksigcopier.APKSigCopierError as e: logging.info('...NOT verified - {0}'.format(tmp_apk)) error = 'signature copying failed: {}'.format(str(e))