diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 89ee08f6..903b6408 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -39,6 +39,9 @@ jobs: sudo chmod a+x /usr/local/bin/apktool sudo wget -q "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar" -O /usr/local/bin/apktool.jar sudo chmod a+x /usr/local/bin/apktool.jar + # Install BundleDecompiler. + sudo wget -q https://github.com/TamilanPeriyasamy/BundleDecompiler/raw/master/build/libs/BundleDecompiler-0.0.2.jar -O /usr/local/bin/BundleDecompiler.jar + sudo chmod a+x /usr/local/bin/BundleDecompiler.jar - name: Run tests run: | diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index b21b11ae..f8d95f3f 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -39,6 +39,8 @@ jobs: sudo chmod a+x /usr/local/bin/apktool sudo wget -q "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar" -O /usr/local/bin/apktool.jar sudo chmod a+x /usr/local/bin/apktool.jar + sudo wget -q https://github.com/TamilanPeriyasamy/BundleDecompiler/raw/master/build/libs/BundleDecompiler-0.0.2.jar -O /usr/local/bin/BundleDecompiler.jar + sudo chmod a+x /usr/local/bin/BundleDecompiler.jar - name: Run tests run: | diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 8d2a9f03..2199d887 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Windows Build Status](https://github.com/ClaudiuGeorgiu/Obfuscapk/workflows/Windows/badge.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/actions?query=workflow%3AWindows) [![MacOS Build Status](https://github.com/ClaudiuGeorgiu/Obfuscapk/workflows/MacOS/badge.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/actions?query=workflow%3AMacOS) [![Docker Hub](https://img.shields.io/docker/cloud/build/claudiugeorgiu/obfuscapk)](https://hub.docker.com/r/claudiugeorgiu/obfuscapk) -[![Python Version](https://img.shields.io/badge/Python-3.6%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/) +[![Python Version](https://img.shields.io/badge/Python-3.7%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/LICENSE) @@ -20,6 +20,22 @@ obfuscated app retains the same functionality as the original one, but the diffe under the hood sometimes make the new application very different from the original (e.g., to signature-based antivirus software). +### :new: Android App Bundle support :new: + +Obfuscapk is adding support for +[Android App Bundles](https://developer.android.com/guide/app-bundle) (aab files) by +using [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler) (see +[#121](https://github.com/ClaudiuGeorgiu/Obfuscapk/pull/121)). In order to use this new +feature, download the latest version of BundleDecompiler available from +[here](https://github.com/TamilanPeriyasamy/BundleDecompiler/tree/master/build/libs) and +save it as `BundleDecompiler.jar` in a directory included in `PATH` (e.g., in Ubuntu, +`/usr/local/bin` or `/usr/bin`). + +`NOTE:` BundleDecompiler doesn't work on Windows yet, so app bundle obfuscation is not +supported by Obfuscapk on Windows platform. Also, app bundle support is still in early +development, so if you faced any problems or if you want to help us improve, please see +[contributing](#-contributing). + ## ❱ Publication @@ -109,7 +125,7 @@ Docker version 19.03.0, build aeac949 #### Official Docker Hub image The [official Obfuscapk Docker image](https://hub.docker.com/r/claudiugeorgiu/obfuscapk) -is available on Docker Hub (automatically built from this repository): +is available on Docker Hub: ```Shell $ # Download the Docker image. @@ -136,7 +152,7 @@ installed correctly: ```Shell $ docker run --rm -it obfuscapk --help -usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] +usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] ... ``` @@ -174,15 +190,22 @@ Copyright (C) 2009 The Android Open Source Project ... ``` -To install and use `apktool` you need a recent version of Java. +To support app bundles obfuscation you need +[BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler), so download +the latest available version from +[here](https://github.com/TamilanPeriyasamy/BundleDecompiler/tree/master/build/libs) and +save it as `BundleDecompiler.jar` in a directory included in `PATH` (e.g., in Ubuntu, +`/usr/local/bin` or `/usr/bin`). + +To use BundleDecompiler and `apktool` you also need a recent version of Java. `zipalign` and `apksigner` are included in the Android SDK. The location of the executables can also be specified through the following environment variables: -`APKTOOL_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., in Ubuntu, run -`export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk in the same -terminal). +`APKTOOL_PATH`, `BUNDLE_DECOMPILER_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., +in Ubuntu, run `export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk +in the same terminal). Apart from the above tools, the only requirement of this project is a working -`Python 3` (at least `3.6`) installation (along with its package manager `pip`). +`Python 3` (at least `3.7`) installation (along with its package manager `pip`). #### Install @@ -209,7 +232,7 @@ $ cd src/ $ # The following command has to be executed always from Obfuscapk/src/ directory $ # or by adding Obfuscapk/src/ directory to PYTHONPATH environment variable. $ python3 -m obfuscapk.cli --help -usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] +usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] ... ``` @@ -249,17 +272,17 @@ Let's start by looking at the help message: ```Shell $ obfuscapk --help -obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] [-i] [-p] [-k VT_API_KEY] +obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] [-i] [-p] [-k VT_API_KEY] [--keystore-file KEYSTORE_FILE] [--keystore-password KEYSTORE_PASSWORD] [--key-alias KEY_ALIAS] [--key-password KEY_PASSWORD] [--use-aapt2] - <APK_FILE> + <APK_OR_BUNDLE_FILE> ``` -There are two mandatory parameters: `<APK_FILE>`, the path (relative or absolute) to -the apk file to obfuscate and the list with the names of the obfuscation techniques to -apply (specified with a `-o` option that can be used multiple times, e.g., -`-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments are as -follows: +There are two mandatory parameters: `<APK_OR_BUNDLE_FILE>`, the path (relative or +absolute) to the apk or app bundle file to obfuscate and the list with the names of the +obfuscation techniques to apply (specified with a `-o` option that can be used multiple +times, e.g., `-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments +are as follows: * `-w DIR` is used to set the working directory where to save the intermediate files (generated by `apktool`). If not specified, a directory named `obfuscation_working_dir` @@ -267,10 +290,11 @@ is created in the same directory as the input application. This can be useful fo debugging purposes, but if it's not needed it can be set to a temporary directory (e.g., `-w /tmp/`). -* `-d OUT_APK` is used to set the path of the destination file: the apk file generated -by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk`). If not -specified, the final obfuscated file will be saved inside the working directory. -Note: existing files will be overwritten without any warning. +* `-d OUT_APK_OR_AAB` is used to set the path of the destination file: the apk file +generated by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk` or +`-d /home/user/Desktop/obfuscated.aab`). If not specified, the final obfuscated file +will be saved inside the working directory. Note: existing files will be overwritten +without any warning. * `-i` is a flag for ignoring known third party libraries during the obfuscation process, to use fewer resources, to increase performances and to reduce the risk of @@ -302,8 +326,8 @@ shown in the example below: com.mycompany.dontobfuscate com.mycompany.ignore ... - ``` -* `--use-aapt2` is a flag for use aapt2 option to rebuild app when using apktool. + ``` +* `--use-aapt2` is a flag for using aapt2 option when rebuilding an app with `apktool`. Let's consider now a simple working example to see how Obfuscapk works: diff --git a/src/obfuscapk/cli.py b/src/obfuscapk/cli.py index bad56459..a380bd7b 100644 --- a/src/obfuscapk/cli.py +++ b/src/obfuscapk/cli.py @@ -21,13 +21,14 @@ def get_cmd_args(args: list = None): parser = argparse.ArgumentParser( prog="python3 -m obfuscapk.cli", - description="Obfuscate an application (.apk) without needing its source code.", + description="Obfuscate an application (.apk/.aab) without needing its " + "source code.", ) parser.add_argument( "apk_file", type=str, - metavar="<APK_FILE>", - help="The path to the application (.apk) to obfuscate", + metavar="<APK_OR_BUNDLE_FILE>", + help="The path to the application (.apk/.aab) to obfuscate", ) parser.add_argument( "-o", @@ -54,9 +55,9 @@ def get_cmd_args(args: list = None): "-d", "--destination", type=str, - metavar="OUT_APK", - help="The path where to save the obfuscated .apk file. By default the file " - "will be saved in the working directory", + metavar="OUT_APK_OR_AAB", + help="The path where to save the obfuscated .apk/.aab file. By default the " + "file will be saved in the working directory", ) parser.add_argument( "-i", diff --git a/src/obfuscapk/main.py b/src/obfuscapk/main.py index aaa7ddf9..a31a2cf7 100644 --- a/src/obfuscapk/main.py +++ b/src/obfuscapk/main.py @@ -8,6 +8,7 @@ from obfuscapk.obfuscation import Obfuscation from obfuscapk.obfuscator_manager import ObfuscatorManager from obfuscapk.tool import Apktool, Zipalign, ApkSigner +from obfuscapk.toolbundledecompiler import BundleDecompiler if "LOG_LEVEL" in os.environ: log_level = os.environ["LOG_LEVEL"] @@ -38,6 +39,7 @@ def check_external_tool_dependencies(): # an exception will be thrown by the corresponding constructor. logger.debug("Checking external tool dependencies") Apktool() + BundleDecompiler() ApkSigner() Zipalign() diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py index 59b77deb..ca7ce11b 100644 --- a/src/obfuscapk/obfuscation.py +++ b/src/obfuscapk/obfuscation.py @@ -7,7 +7,8 @@ from typing import List, Union from obfuscapk import util -from obfuscapk.tool import Apktool, Zipalign, ApkSigner +from obfuscapk.tool import Apktool, ApkSigner, Zipalign +from obfuscapk.toolbundledecompiler import BundleDecompiler, AABSigner class Obfuscation(object): @@ -46,6 +47,10 @@ def __init__( self.key_password: str = key_password self.ignore_packages_file: str = ignore_packages_file self.use_aapt2 = use_aapt2 + if apk_path.endswith('aab'): + self.is_bundle = True + else: + self.is_bundle = False # Random string (32 chars long) generation with ASCII letters and digits self.encryption_secret = "".join( @@ -114,12 +119,20 @@ def __init__( # If the path of the output obfuscated apk is not specified, save it in the # working directory. if not self.obfuscated_apk_path: - self.obfuscated_apk_path = "{0}_obfuscated.apk".format( - os.path.join( - self.working_dir_path, - os.path.splitext(os.path.basename(self.apk_path))[0], + if (self.is_bundle): + self.obfuscated_apk_path = "{0}_obfuscated.aab".format( + os.path.join( + self.working_dir_path, + os.path.splitext(os.path.basename(self.apk_path))[0], + ) + ) + else: + self.obfuscated_apk_path = "{0}_obfuscated.apk".format( + os.path.join( + self.working_dir_path, + os.path.splitext(os.path.basename(self.apk_path))[0], + ) ) - ) self.logger.debug( "No obfuscated apk path provided, the result will be saved " 'as "{0}"'.format(self.obfuscated_apk_path) @@ -329,8 +342,9 @@ def decode_apk(self) -> None: if not self._is_decoded: - # The input apk will be decoded with apktool. + # The input apk will be decoded with apktool or BundleDecompiler. apktool: Apktool = Apktool() + bundledecompiler: BundleDecompiler = BundleDecompiler() # <working_directory>/<apk_path>/ self._decoded_apk_path = os.path.join( @@ -338,15 +352,24 @@ def decode_apk(self) -> None: os.path.splitext(os.path.basename(self.apk_path))[0], ) try: - apktool.decode(self.apk_path, self._decoded_apk_path, force=True) + if (self.is_bundle): + bundledecompiler.decode(self.apk_path, self._decoded_apk_path, force=False) + else: + apktool.decode(self.apk_path, self._decoded_apk_path, force=True) + # Path to the decoded manifest file. - self._manifest_file = os.path.join( + if (self.is_bundle): + self._manifest_file = os.path.join( + self._decoded_apk_path, "base", "manifest", "AndroidManifest.xml", + ) + else: + self._manifest_file = os.path.join( self._decoded_apk_path, "AndroidManifest.xml" ) # A list containing the paths to all the smali files obtained with - # apktool. + # apktool or bundledecompiler. self._smali_files = [ os.path.join(root, file_name) for root, dir_names, file_names in os.walk(self._decoded_apk_path) @@ -389,19 +412,31 @@ def decode_apk(self) -> None: self._smali_files.sort() # Check if multidex. - if os.path.isdir( - os.path.join(self._decoded_apk_path, "smali_classes2") - ): - self._is_multidex = True - + if (self.is_bundle): + if os.path.isdir( + os.path.join(self._decoded_apk_path, "base", "dex", "smali_classes2") + ): + self._is_multidex = True + else: + if os.path.isdir( + os.path.join(self._decoded_apk_path, "smali_classes2") + ): + self._is_multidex = True + + if (self._is_multidex): smali_directories = ["smali"] for i in range(2, 15): smali_directories.append("smali_classes{0}".format(i)) for smali_directory in smali_directories: - current_directory = os.path.join( - self._decoded_apk_path, smali_directory, "" - ) + if (self.is_bundle): + current_directory = os.path.join( + self._decoded_apk_path, "base", "dex", smali_directory, "" + ) + else: + current_directory = os.path.join( + self._decoded_apk_path, smali_directory, "" + ) if os.path.isdir(current_directory): self._multidex_smali_files.append( [ @@ -499,13 +534,15 @@ def build_obfuscated_apk(self) -> None: if not self._is_decoded: self.decode_apk() - # The obfuscated apk will be built with apktool. + # The obfuscated apk will be built with apktool or BundleDecompiler. apktool: Apktool = Apktool() + bundledecompiler: BundleDecompiler = BundleDecompiler() try: - apktool.build( - self._decoded_apk_path, self.obfuscated_apk_path, self.use_aapt2 - ) + if (self.is_bundle): + bundledecompiler.build(self._decoded_apk_path, self.obfuscated_apk_path) + else: + apktool.build(self._decoded_apk_path, self.obfuscated_apk_path) except Exception as e: self.logger.error("Error during apk building: {0}".format(e)) raise @@ -514,7 +551,8 @@ def sign_obfuscated_apk(self) -> None: # This method must be called AFTER the obfuscated apk has been built. - # The obfuscated apk will be signed with apksigner. + # The obfuscated apk will be signed with APKSigner or BundleDecompiler. + aabsigner: AABSigner = AABSigner() apksigner: ApkSigner = ApkSigner() # If a custom keystore file is not provided, use the default one bundled with @@ -541,13 +579,18 @@ def sign_obfuscated_apk(self) -> None: ) try: - apksigner.resign( - self.obfuscated_apk_path, - self.keystore_file, - self.keystore_password, - self.key_alias, - self.key_password, - ) + if (self.is_bundle): + aabsigner.sign( + self.obfuscated_apk_path, + ) + else: + apksigner.resign( + self.obfuscated_apk_path, + self.keystore_file, + self.keystore_password, + self.key_alias, + self.key_password, + ) except Exception as e: self.logger.error("Error during apk signing: {0}".format(e)) raise @@ -558,6 +601,8 @@ def align_obfuscated_apk(self) -> None: # The obfuscated apk will be aligned with zipalign. zipalign: Zipalign = Zipalign() + if (self.is_bundle): + return try: zipalign.align(self.obfuscated_apk_path) @@ -607,7 +652,10 @@ def get_assets_directory(self) -> str: self.decode_apk() # '.join(x, "")' is used to add a trailing slash. - return os.path.join(self._decoded_apk_path, "assets", "") + if (self.is_bundle): + return os.path.join(self._decoded_apk_path, "base", "assets", "") + else: + return os.path.join(self._decoded_apk_path, "assets", "") def get_resource_directory(self) -> str: @@ -615,7 +663,11 @@ def get_resource_directory(self) -> str: self.decode_apk() # '.join(x, "")' is used to add a trailing slash. - return os.path.join(self._decoded_apk_path, "res", "") + if (self.is_bundle): + return os.path.join(self._decoded_apk_path, "base", "res", "") + else: + return os.path.join(self._decoded_apk_path, "res", "") + def get_ignore_package_names(self) -> List[str]: ignore_package_list = [] diff --git a/src/obfuscapk/toolbundledecompiler.py b/src/obfuscapk/toolbundledecompiler.py new file mode 100644 index 00000000..8fc450ed --- /dev/null +++ b/src/obfuscapk/toolbundledecompiler.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 + +import logging +import os +import platform +import shutil +import subprocess +from typing import List + + +class BundleDecompiler(object): + def __init__(self): + self.logger = logging.getLogger( + "{0}.{1}".format(__name__, self.__class__.__name__) + ) + + if platform.system() == "Windows": + self.logger.warning("BundleDecompiler is not yet available on Windows platform") + return + + if "BUNDLE_DECOMPILER_PATH" in os.environ: + self.bundledecompiler_path: str = os.environ["BUNDLE_DECOMPILER_PATH"] + else: + self.bundledecompiler_path: str = "BundleDecompiler.jar" + + full_bundledecompiler_path = shutil.which(self.bundledecompiler_path) + + # Make sure bundle decompiler is available + if not os.path.isfile(full_bundledecompiler_path): + raise RuntimeError( + 'Cannot find BundleDecompiler with executable "{0}"'.format(full_bundledecompiler_path) + ) + + # Make sure to use the full path of the executable (needed for cross-platform + # compatibility). + if full_bundledecompiler_path is None: + raise RuntimeError( + 'Something is wrong with executable "{0}"'.format(self.bundledecompiler_path) + ) + else: + self.bundledecompiler_path = full_bundledecompiler_path + + def decode( + self, aab_path: str, output_dir_path: str = None, force: bool = False + ) -> str: + if platform.system() == "Windows": + raise NotImplementedError("BundleDecompiler is not yet available on Windows platform") + + # Check if the aab file to decode is a valid file. + if not os.path.isfile(aab_path): + self.logger.error('Unable to find file "{0}"'.format(aab_path)) + raise FileNotFoundError('Unable to find file "{0}"'.format(aab_path)) + + # If no output directory is specified, use a new directory in the same + # directory as the aab file to decode. + if not output_dir_path: + output_dir_path = os.path.join( + os.path.dirname(aab_path), + os.path.splitext(os.path.basename(aab_path))[0], + ) + self.logger.debug( + "No output directory provided, the result will be saved in the " + "same directory as the input file, in a directory with the same " + 'name as the input file: "{0}"'.format(output_dir_path) + ) + + # If an output directory is provided, make sure that the path to that + # directory exists (the final directory will be created by aabtool). + elif not os.path.isdir(os.path.dirname(output_dir_path)): + self.logger.error( + 'Unable to find output directory "{0}", aabtool won\'t be able to ' + 'create the directory "{1}"'.format( + os.path.dirname(output_dir_path), output_dir_path + ) + ) + raise NotADirectoryError( + 'Unable to find output directory "{0}", aabtool won\'t be able to ' + 'create the directory "{1}"'.format( + os.path.dirname(output_dir_path), output_dir_path + ) + ) + + # Inform the user if an existing output directory is provided without the + # "force" flag. + if os.path.isdir(output_dir_path) and not force: + self.logger.error( + 'Output directory "{0}" already exists, use the "force" flag ' + "to overwrite".format(output_dir_path) + ) + raise FileExistsError( + 'Output directory "{0}" already exists, use the "force" flag ' + "to overwrite".format(output_dir_path) + ) + + decode_cmd: List[str] = [ + "java", + "-jar", + self.bundledecompiler_path, + "d", + "--in=" + aab_path, + "--out=" + output_dir_path, + ] + + if force: + self.logger.warning( + 'Bundle Decompiler does not support force' + ) + + try: + self.logger.info( + 'Running decode command "{0}"'.format(" ".join(decode_cmd)) + ) + # A new line character is sent as input since newer versions of aabtool + # have an interactive prompt on Windows where the user should press a key. + output = subprocess.check_output( + decode_cmd, stderr=subprocess.STDOUT, input=b"\n" + ).strip() + if b"Exception in thread " in output: + # Report exception raised in aabtool. + raise subprocess.CalledProcessError(1, decode_cmd, output) + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during decode command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during decoding: {0}".format(e)) + raise + + def build(self, source_dir_path: str, output_aab_path: str = None) -> str: + if platform.system() == "Windows": + raise NotImplementedError("BundleDecompiler is not yet available on Windows platform") + + # Check if the input directory exists. + if not os.path.isdir(source_dir_path): + self.logger.error( + 'Unable to find source directory "{0}"'.format(source_dir_path) + ) + raise NotADirectoryError( + 'Unable to find source directory "{0}"'.format(source_dir_path) + ) + + # If no output aab path is specified, the new aab will be saved in the + # default path: <source_dir_path>/dist/<source_dir_name>.aab + if not output_aab_path: + output_aab_path = os.path.join( + source_dir_path, + "output", + "{0}.aab".format(os.path.basename(source_dir_path)), + ) + self.logger.debug( + "No output aab path provided, the new aab will be saved in the " + 'default path: "{0}"'.format(output_aab_path) + ) + + build_cmd: List[str] = [ + "java", + "-jar", + self.bundledecompiler_path, + "b", + "--in=" + source_dir_path, + "--out=" + output_aab_path, + ] + + try: + self.logger.info('Running build command "{0}"'.format(" ".join(build_cmd))) + # A new line character is sent as input since newer versions of aabtool + # have an interactive prompt on Windows where the user should press a key. + output = subprocess.check_output( + build_cmd, stderr=subprocess.STDOUT, input=b"\n" + ).strip() + if ( + b"brut.directory.PathNotExist: " in output + or b"Exception in thread " in output + ): + # Report exception raised in aabtool. + raise subprocess.CalledProcessError(1, build_cmd, output) + + if not os.path.isfile(output_aab_path): + raise FileNotFoundError( + '"{0}" was not built correctly. aabtool output:\n{1}'.format( + output_aab_path, output.decode(errors="replace") + ) + ) + + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during build command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during building: {0}".format(e)) + raise + + +class AABSigner(object): + def __init__(self): + self.logger = logging.getLogger( + "{0}.{1}".format(__name__, self.__class__.__name__) + ) + + if platform.system() == "Windows": + self.logger.warning("BundleDecompiler is not yet available on Windows platform") + return + + if "BUNDLE_DECOMPILER_PATH" in os.environ: + self.aabsigner_path: str = os.environ["BUNDLE_DECOMPILER_PATH"] + else: + self.aabsigner_path: str = "BundleDecompiler.jar" + + full_aabsigner_path = shutil.which(self.aabsigner_path) + + # Make sure to use the full path of the executable (needed for cross-platform + # compatibility). + if full_aabsigner_path is None: + raise RuntimeError( + 'Something is wrong with executable "{0}"'.format(self.aabsigner_path) + ) + else: + self.aabsigner_path = full_aabsigner_path + + def sign( + self, + aab_path: str, + ) -> str: + if platform.system() == "Windows": + raise NotImplementedError("BundleDecompiler is not yet available on Windows platform") + + # Check if the aab file to sign is a valid file. + if not os.path.isfile(aab_path): + self.logger.error('Unable to find file "{0}"'.format(aab_path)) + raise FileNotFoundError('Unable to find file "{0}"'.format(aab_path)) + + sign_cmd: List[str] = [ + "java", + "-jar", + self.aabsigner_path, + "sign-bundle", + "--in=" + aab_path, + "--out=" + aab_path.replace(".aab","_signed.aab") + ] + + try: + self.logger.info('Running sign command "{0}"'.format(" ".join(sign_cmd))) + output = subprocess.check_output(sign_cmd, stderr=subprocess.STDOUT).strip() + return output.decode(errors="replace") + except subprocess.CalledProcessError as e: + self.logger.error( + "Error during sign command: {0}".format( + e.output.decode(errors="replace") if e.output else e + ) + ) + raise + except Exception as e: + self.logger.error("Error during signing: {0}".format(e)) + raise