From cdc7c98707d7b3fba8f25e9a7e10e88c035cc98e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 1 Apr 2024 11:42:23 +0200 Subject: [PATCH 1/6] common.get_androguard_APK() is no longer private to the module --- fdroidserver/common.py | 6 +++--- tests/common.TestCase | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index c7c4c5c2..3c7d7dc0 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -2647,7 +2647,7 @@ def use_androguard(): use_androguard.show_path = True # type: ignore -def _get_androguard_APK(apkfile): +def get_androguard_APK(apkfile): try: from androguard.core.bytecodes.apk import APK except ImportError as exc: @@ -2793,7 +2793,7 @@ def get_apk_id_androguard(apkfile): .format(path=apkfile)) if not versionName or versionName[0] == '@': - a = _get_androguard_APK(apkfile) + a = get_androguard_APK(apkfile) versionName = ensure_final_value(a.package, a.get_android_resources(), a.get_androidversion_name()) if not versionName: versionName = '' # versionName is expected to always be a str @@ -3160,7 +3160,7 @@ def get_first_signer_certificate(apkpath): cert_encoded = get_certificate(apk.read(cert_files[0])) if not cert_encoded and use_androguard(): - apkobject = _get_androguard_APK(apkpath) + apkobject = get_androguard_APK(apkpath) certs = apkobject.get_certificates_der_v2() if len(certs) > 0: logging.debug(_('Using APK Signature v2')) diff --git a/tests/common.TestCase b/tests/common.TestCase index 543e8eb6..925d61e0 100755 --- a/tests/common.TestCase +++ b/tests/common.TestCase @@ -902,7 +902,7 @@ class CommonTest(unittest.TestCase): self.assertTrue(os.path.isfile(signed)) self.assertFalse(os.path.isfile(unsigned)) self.assertTrue(fdroidserver.common.verify_apk_signature(signed)) - self.assertEqual('18', fdroidserver.common._get_androguard_APK(signed).get_min_sdk_version()) + self.assertEqual('18', fdroidserver.common.get_androguard_APK(signed).get_min_sdk_version()) shutil.copy(os.path.join(self.basedir, 'minimal_targetsdk_30_unsigned.apk'), self.testdir) unsigned = os.path.join(self.testdir, 'minimal_targetsdk_30_unsigned.apk') @@ -915,7 +915,7 @@ class CommonTest(unittest.TestCase): self.assertFalse(os.path.isfile(unsigned)) self.assertTrue(fdroidserver.common.verify_apk_signature(signed)) # verify it has a v2 signature - self.assertTrue(fdroidserver.common._get_androguard_APK(signed).is_signed_v2()) + self.assertTrue(fdroidserver.common.get_androguard_APK(signed).is_signed_v2()) shutil.copy(os.path.join(self.basedir, 'no_targetsdk_minsdk30_unsigned.apk'), self.testdir) unsigned = os.path.join(self.testdir, 'no_targetsdk_minsdk30_unsigned.apk') @@ -923,7 +923,7 @@ class CommonTest(unittest.TestCase): fdroidserver.common.sign_apk(unsigned, signed, config['keyalias']) self.assertTrue(fdroidserver.common.verify_apk_signature(signed)) - self.assertTrue(fdroidserver.common._get_androguard_APK(signed).is_signed_v2()) + self.assertTrue(fdroidserver.common.get_androguard_APK(signed).is_signed_v2()) shutil.copy(os.path.join(self.basedir, 'no_targetsdk_minsdk1_unsigned.apk'), self.testdir) unsigned = os.path.join(self.testdir, 'no_targetsdk_minsdk1_unsigned.apk') @@ -1146,11 +1146,11 @@ class CommonTest(unittest.TestCase): """This is a sanity test that androguard isn't broken""" def get_minSdkVersion(apkfile): - apk = fdroidserver.common._get_androguard_APK(apkfile) + apk = fdroidserver.common.get_androguard_APK(apkfile) return fdroidserver.common.get_min_sdk_version(apk) def get_targetSdkVersion(apkfile): - apk = fdroidserver.common._get_androguard_APK(apkfile) + apk = fdroidserver.common.get_androguard_APK(apkfile) return apk.get_effective_target_sdk_version() self.assertEqual(4, get_minSdkVersion('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')) From 7a144a47626b9d2426ee01adb5f3a827fe9330b7 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 1 Apr 2024 11:42:23 +0200 Subject: [PATCH 2/6] port to androguard >= 4 and drop support for older than 3.3.3 This also makes androguard a hard requirement, which has been true for a while anyway. So the code that handles androguard as an optional requirement is removed. androguard from Debian/buster is new enough, so this does not seem like it will cause any problems. --- fdroidserver/common.py | 40 +++++++++++++++++----------------------- fdroidserver/update.py | 4 +--- setup.py | 2 +- tests/run-tests | 4 +++- tests/update.TestCase | 15 ++++++++------- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 3c7d7dc0..f4da28a4 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -2629,29 +2629,12 @@ def get_file_extension(filename): return os.path.splitext(filename)[1].lower()[1:] -def use_androguard(): - """Report if androguard is available, and config its debug logging.""" - try: - import androguard - if use_androguard.show_path: - logging.debug(_('Using androguard from "{path}"').format(path=androguard.__file__)) - use_androguard.show_path = False - if options and options.verbose: - logging.getLogger("androguard.axml").setLevel(logging.INFO) - logging.getLogger("androguard.core.api_specific_resources").setLevel(logging.ERROR) - return True - except ImportError: - return False - - -use_androguard.show_path = True # type: ignore - - def get_androguard_APK(apkfile): try: + # these were moved in androguard 4.0 + from androguard.core.apk import APK + except ImportError: from androguard.core.bytecodes.apk import APK - except ImportError as exc: - raise FDroidException("androguard library is not installed") from exc return APK(apkfile) @@ -2693,7 +2676,11 @@ def is_debuggable_or_testOnly(apkfile): """ if get_file_extension(apkfile) != 'apk': return False - from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG + try: + # these were moved in androguard 4.0 + from androguard.core.axml import AXMLParser, format_value, START_TAG + except ImportError: + from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG with ZipFile(apkfile) as apk: with apk.open('AndroidManifest.xml') as manifest: axml = AXMLParser(manifest.read()) @@ -2753,12 +2740,19 @@ def get_apk_id_androguard(apkfile): versionName is set to a Android String Resource (e.g. an integer hex value that starts with @). + This function is part of androguard as get_apkid(), so this + vendored and modified to return versionCode as an integer. + """ if not os.path.exists(apkfile): raise FDroidException(_("Reading packageName/versionCode/versionName failed, APK invalid: '{apkfilename}'") .format(apkfilename=apkfile)) - from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT + try: + # these were moved in androguard 4.0 + from androguard.core.axml import AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT + except ImportError: + from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT appid = None versionCode = None @@ -3159,7 +3153,7 @@ def get_first_signer_certificate(apkpath): elif len(cert_files) == 1: cert_encoded = get_certificate(apk.read(cert_files[0])) - if not cert_encoded and use_androguard(): + if not cert_encoded: apkobject = get_androguard_APK(apkpath) certs = apkobject.get_certificates_der_v2() if len(certs) > 0: diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 23e3d604..be3824bd 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -1722,8 +1722,7 @@ def _sanitize_sdk_version(value): def scan_apk_androguard(apk, apkfile): try: - from androguard.core.bytecodes.apk import APK - apkobject = APK(apkfile) + apkobject = common.get_androguard_APK(apkfile) if apkobject.is_valid_APK(): arsc = apkobject.get_android_resources() else: @@ -2581,7 +2580,6 @@ def main(): config = common.read_config(options) common.setup_status_output(start_timestamp) - common.use_androguard() if not (('jarsigner' in config or 'apksigner' in config) and 'keytool' in config): raise FDroidException(_('Java JDK not found! Install in standard location or set java_paths!')) diff --git a/setup.py b/setup.py index 49548f78..94e43e98 100755 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ setup( ], install_requires=[ 'appdirs', - 'androguard >= 3.1.0, != 3.3.0, != 3.3.1, != 3.3.2, <4', + 'androguard >= 3.3.3', 'clint', 'defusedxml', 'GitPython', diff --git a/tests/run-tests b/tests/run-tests index ca9aa951..31cb4939 100755 --- a/tests/run-tests +++ b/tests/run-tests @@ -8,7 +8,9 @@ echo_header() { get_fdroid_apk_filename() { if [ -z $aapt ]; then - python3 -c "from androguard.core.bytecodes.apk import APK; a=APK('$1'); print(a.package+'_'+a.get_androidversion_code()+'.apk')" + appid=$(androguard apkid "$1" | sed -En 's/ +"([a-z][^"]+)",$/\1/ip') + versionCode=$(androguard apkid "$1" | sed -En 's/ +"([0-9]+)",$/\1/p') + echo "${appid}_${versionCode}.apk" else $aapt dump badging "$1" | sed -n "s,^package: name='\(.*\)' versionCode='\([0-9][0-9]*\)' .*,\1_\2.apk,p" fi diff --git a/tests/update.TestCase b/tests/update.TestCase index 2ab19a3a..232537ca 100755 --- a/tests/update.TestCase +++ b/tests/update.TestCase @@ -5,6 +5,7 @@ import copy import git import glob +import hashlib import inspect import json import logging @@ -20,11 +21,18 @@ import unittest import yaml import zipfile import textwrap +from binascii import hexlify from datetime import datetime from pathlib import Path from testcommon import TmpCwd, mkdtemp from unittest import mock +try: + # these were moved in androguard 4.0 + from androguard.core.apk import APK +except ImportError: + from androguard.core.bytecodes.apk import APK + try: from yaml import CSafeLoader as SafeLoader except ImportError: @@ -581,13 +589,6 @@ class UpdateTest(unittest.TestCase): self.assertEqual(good_fingerprint, sig, 'python sig was: ' + str(sig)) # check that v1 and v2 have the same certificate - try: - import hashlib - from binascii import hexlify - from androguard.core.bytecodes.apk import APK - except ImportError: - print('WARNING: skipping rest of test since androguard is missing!') - return apkobject = APK(apkpath) cert_encoded = apkobject.get_certificates_der_v2()[0] self.assertEqual(good_fingerprint, sig, From 5b7abc04235418ef6d721fe3e4b408e9e49ebe0c Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 24 Apr 2024 11:14:14 +0200 Subject: [PATCH 3/6] single function to tame androguard's verbose default output # Conflicts: # fdroidserver/common.py --- fdroidserver/common.py | 30 ++++++++++++++++++++++++++++++ tests/build.TestCase | 2 -- tests/update.TestCase | 3 --- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index f4da28a4..5f01f9a9 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -2629,12 +2629,39 @@ def get_file_extension(filename): return os.path.splitext(filename)[1].lower()[1:] +def _androguard_logging_level(level=logging.ERROR): + """Tames androguard's default debug output. + + There should be no debug output when the functions are being used + via the API. Otherwise, the output is controlled by the --verbose + flag. + + To get coverage across the full range of androguard >= 3.3.5, this + includes all known logger names that are relevant. So some of + these names might not be present in the version of androguard + currently in use. + + """ + if options and options.verbose: + level = logging.WARNING + + for name in ( + 'androguard.apk', + 'androguard.axml', + 'androguard.core.api_specific_resources', + 'androguard.core.apk', + 'androguard.core.axml', + ): + logging.getLogger(name).setLevel(level) + + def get_androguard_APK(apkfile): try: # these were moved in androguard 4.0 from androguard.core.apk import APK except ImportError: from androguard.core.bytecodes.apk import APK + _androguard_logging_level() return APK(apkfile) @@ -2681,6 +2708,8 @@ def is_debuggable_or_testOnly(apkfile): from androguard.core.axml import AXMLParser, format_value, START_TAG except ImportError: from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG + _androguard_logging_level() + with ZipFile(apkfile) as apk: with apk.open('AndroidManifest.xml') as manifest: axml = AXMLParser(manifest.read()) @@ -2753,6 +2782,7 @@ def get_apk_id_androguard(apkfile): from androguard.core.axml import AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT except ImportError: from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT + _androguard_logging_level() appid = None versionCode = None diff --git a/tests/build.TestCase b/tests/build.TestCase index 6a4ddb02..3ad4cdd0 100755 --- a/tests/build.TestCase +++ b/tests/build.TestCase @@ -49,8 +49,6 @@ class BuildTest(unittest.TestCase): def setUp(self): logging.basicConfig(level=logging.DEBUG) - logger = logging.getLogger('androguard.axml') - logger.setLevel(logging.INFO) # tame the axml debug messages self.basedir = os.path.join(localmodule, 'tests') os.chdir(self.basedir) fdroidserver.common.config = None diff --git a/tests/update.TestCase b/tests/update.TestCase index 232537ca..0c692aa8 100755 --- a/tests/update.TestCase +++ b/tests/update.TestCase @@ -82,9 +82,6 @@ class UpdateTest(unittest.TestCase): def setUp(self): logging.basicConfig(level=logging.INFO) - logging.getLogger('androguard.apk').setLevel(logging.WARNING) - logging.getLogger('androguard.axml').setLevel(logging.INFO) - logging.getLogger('androguard.core.api_specific_resources').setLevel(logging.INFO) from PIL import PngImagePlugin logging.getLogger(PngImagePlugin.__name__).setLevel(logging.INFO) From 1c84f63247cff50b08ed15dec59b1c6b4a81c9d0 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 10 Apr 2024 16:51:24 +0200 Subject: [PATCH 4/6] replace deprecated get_element() which was removed in 4.x /usr/lib/python3/dist-packages/androguard/core/bytecodes/apk.py:884: DeprecationWarning: This method is deprecated since 3.3.5. It was added in 3.3.5. Debian/bullseye and Ubuntu/20.04/focal both include new enough versions. Debian/buster's is too old (3.3.3). --- fdroidserver/update.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fdroidserver/update.py b/fdroidserver/update.py index be3824bd..1c38ff90 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -1778,7 +1778,7 @@ def scan_apk_androguard(apk, apkfile): if maxSdkVersion is not None: apk['maxSdkVersion'] = maxSdkVersion - icon_id_str = apkobject.get_element("application", "icon") + icon_id_str = apkobject.get_attribute_value("application", "icon") if icon_id_str: try: icon_id = int(icon_id_str.replace("@", "0x"), 16) diff --git a/setup.py b/setup.py index 94e43e98..8e4e2452 100755 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ setup( ], install_requires=[ 'appdirs', - 'androguard >= 3.3.3', + 'androguard >= 3.3.5', 'clint', 'defusedxml', 'GitPython', From ef4ec74882e1086643c802e1082b514fe8a2a31b Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 24 Apr 2024 15:45:15 +0200 Subject: [PATCH 5/6] some parts of androguard 4.x use loguru instead of logging --- fdroidserver/common.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 5f01f9a9..c90db883 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -2654,6 +2654,13 @@ def _androguard_logging_level(level=logging.ERROR): ): logging.getLogger(name).setLevel(level) + # some parts of androguard 4.x use loguru instead of logging + try: + from loguru import logger + logger.remove() + except ImportError: + pass + def get_androguard_APK(apkfile): try: From be59b38ac18275e44ab1de52a501426066b08c77 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 25 Apr 2024 12:59:00 +0200 Subject: [PATCH 6/6] update: handle ValueError from apkInspector in androguard 4.1 androguard 4.1 uses a new lib called apkInspector instead of zipfile.ZipFile so that it can handle usable but invalid ZIP files. It will also throw ValueError on some things, for example: Traceback (most recent call last): File "/builds/eighthave/fdroidserver/fdroidserver-2.3a0/tests/update.TestCase", line 878, in test_scan_apk_bad_zip fdroidserver.update.scan_apk(apkfile) File "/builds/eighthave/fdroidserver/fdroidserver-2.3a0/fdroidserver/update.py", line 1586, in scan_apk scan_apk_androguard(apk, apk_file) File "/builds/eighthave/fdroidserver/fdroidserver-2.3a0/fdroidserver/update.py", line 1725, in scan_apk_androguard apkobject = common.get_androguard_APK(apkfile) File "/builds/eighthave/fdroidserver/fdroidserver-2.3a0/fdroidserver/common.py", line 2673, in get_androguard_APK return APK(apkfile) File "/usr/local/lib/python3.10/dist-packages/androguard/core/apk/__init__.py", line 273, in __init__ self.zip = ZipEntry.parse(filename, False) File "/usr/local/lib/python3.10/dist-packages/apkInspector/headers.py", line 410, in parse eocd = EndOfCentralDirectoryRecord.parse(apk_file) File "/usr/local/lib/python3.10/dist-packages/apkInspector/headers.py", line 59, in parse raise ValueError("End of central directory record (EOCD) signature not found") ValueError: End of central directory record (EOCD) signature not found --- fdroidserver/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 1c38ff90..2ce07fa5 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -1738,7 +1738,7 @@ def scan_apk_androguard(apk, apkfile): logging.error(_("Failed to get APK information, skipping {path}") .format(path=apkfile)) raise BuildException(_("Invalid APK")) - except (FileNotFoundError, zipfile.BadZipFile) as e: + except (FileNotFoundError, ValueError, zipfile.BadZipFile) as e: logging.error(_("Could not open APK {path} for analysis: ").format(path=apkfile) + str(e)) raise BuildException(_("Invalid APK")) from e