Skip to content
This repository has been archived by the owner on Jul 27, 2024. It is now read-only.

Replacing Jarsigner with Apksigner #83

Merged
merged 10 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ information.

Make sure to have a recent version of
[`apktool`](https://ibotpeaches.github.io/Apktool/),
[`jarsigner`](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html)
[`apksigner`](https://developer.android.com/studio/command-line/apksigner)
and [`zipalign`](https://developer.android.com/studio/command-line/zipalign) installed
and available from the command line:

Expand All @@ -161,9 +161,10 @@ Apktool v2.5.0 - a tool for reengineering Android apk files
...
```
```Shell
$ jarsigner
Usage: jarsigner [options] jar-file alias
jarsigner -verify [options] jar-file [alias...]
$ apksigner
Usage: apksigner <command> [options]
apksigner --version
apksigner --help
...
```
```Shell
Expand All @@ -173,10 +174,10 @@ Copyright (C) 2009 The Android Open Source Project
...
```

To install and use `apktool` you need a recent version of Java, which should also have
`jarsigner` bundled. `zipalign` is included in the Android SDK. The location of the
To install and use `apktool` you need a recent version of Java.
`zipalign` and `apksigner` is included in the Android SDK. The location of the
ClaudiuGeorgiu marked this conversation as resolved.
Show resolved Hide resolved
executables can also be specified through the following environment variables:
`APKTOOL_PATH`, `JARSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., in Ubuntu, run
`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).

Expand Down Expand Up @@ -257,7 +258,7 @@ obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] [-i] [-p] [-k VT_API_KEY]
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 NewSignature -o NewAlignment`). The other optional arguments are as
`-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
Expand Down Expand Up @@ -307,7 +308,7 @@ Let's consider now a simple working example to see how Obfuscapk works:

```Shell
$ # original.apk is a valid Android apk file.
$ obfuscapk -o RandomManifest -o Rebuild -o NewSignature -o NewAlignment original.apk
$ obfuscapk -o RandomManifest -o Rebuild -o NewAlignment -o NewSignature original.apk
```

When running the above command, this is what happens behind the scenes:
Expand All @@ -332,18 +333,18 @@ available and ready to be used
manifest) using `apktool`, and since no output file was specified, the resulting
apk file is saved in the working directory created before

- `NewSignature` obfuscator signs the newly created apk file with a custom
certificate contained in a
[keystore bundled with Obfuscapk](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/src/obfuscapk/resources/obfuscation_keystore.jks)
(though a different keystore can be specified with the `--keystore-file` parameter)

- `NewAlignment` obfuscator uses `zipalign` tool to align the resulting apk file

- `NewSignature` obfuscator signs the newly created apk file with a custom
certificate contained in a
[keystore bundled with Obfuscapk](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/src/obfuscapk/resources/obfuscation_keystore.jks)
(though a different keystore can be specified with the `--keystore-file` parameter)

* when all the obfuscators have been executed without errors, the resulting obfuscated
apk file can be found in `obfuscation_working_dir/original_obfuscated.apk`, signed,
aligned and ready to be installed into a device/emulator

As seen in the previous example, `Rebuild`, `NewSignature` and `NewAlignment`
As seen in the previous example, `Rebuild`, `NewAlignment` and `NewSignature`
obfuscators are always needed to complete an obfuscation operation, to build the final
obfuscated apk. They are not actual obfuscation techniques, but they are needed in the
build process and so they are included in the list of obfuscators to keep the overall
Expand Down
2 changes: 1 addition & 1 deletion src/obfuscapk/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def main():
-o AssetEncryption -o MethodOverload -o ConstStringEncryption \
-o ResStringEncryption -o ArithmeticBranch -o FieldRename -o Nop -o Goto \
-o ClassRename -o Reflection -o AdvancedReflection -o Reorder -o RandomManifest \
-o Rebuild -o NewSignature -o NewAlignment \
-o Rebuild -o NewAlignment -o NewSignature \
-o VirusTotal -k virus_total_key \
/path/to/original.apk
"""
Expand Down
4 changes: 2 additions & 2 deletions src/obfuscapk/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from obfuscapk import util
from obfuscapk.obfuscation import Obfuscation
from obfuscapk.obfuscator_manager import ObfuscatorManager
from obfuscapk.tool import Apktool, Jarsigner, Zipalign
from obfuscapk.tool import Apktool, Zipalign, ApkSigner

if "LOG_LEVEL" in os.environ:
log_level = os.environ["LOG_LEVEL"]
Expand Down Expand Up @@ -38,7 +38,7 @@ def check_external_tool_dependencies():
# an exception will be thrown by the corresponding constructor.
logger.debug("Checking external tool dependencies")
Apktool()
Jarsigner()
ApkSigner()
Zipalign()


Expand Down
6 changes: 3 additions & 3 deletions src/obfuscapk/obfuscation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import List, Union

from obfuscapk import util
from obfuscapk.tool import Apktool, Jarsigner, Zipalign
from obfuscapk.tool import Apktool, Jarsigner, Zipalign, ApkSigner


class Obfuscation(object):
Expand Down Expand Up @@ -511,7 +511,7 @@ 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 jarsigner.
jarsigner: Jarsigner = Jarsigner()
apksigner: ApkSigner = ApkSigner()

# If a custom keystore file is not provided, use the default one bundled with
# the tool. Otherwise check that the keystore password and a key alias are
Expand All @@ -537,7 +537,7 @@ def sign_obfuscated_apk(self) -> None:
)

try:
jarsigner.resign(
apksigner.resign(
self.obfuscated_apk_path,
self.keystore_file,
self.keystore_password,
Expand Down
165 changes: 80 additions & 85 deletions src/obfuscapk/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,27 +188,90 @@ def build(self, source_dir_path: str, output_apk_path: str = None) -> str:
raise


class Jarsigner(object):
class Zipalign(object):
def __init__(self):
self.logger = logging.getLogger(
"{0}.{1}".format(__name__, self.__class__.__name__)
)

if "ZIPALIGN_PATH" in os.environ:
self.zipalign_path: str = os.environ["ZIPALIGN_PATH"]
else:
self.zipalign_path: str = "zipalign"

full_zipalign_path = shutil.which(self.zipalign_path)

# Make sure to use the full path of the executable (needed for cross-platform
# compatibility).
if full_zipalign_path is None:
raise RuntimeError(
'Something is wrong with executable "{0}"'.format(self.zipalign_path)
)
else:
self.zipalign_path = full_zipalign_path

def align(self, apk_path: str) -> str:

# Check if the apk file to align is a valid file.
if not os.path.isfile(apk_path):
self.logger.error('Unable to find file "{0}"'.format(apk_path))
raise FileNotFoundError('Unable to find file "{0}"'.format(apk_path))

# Since zipalign cannot be run inplace, a temp file will be created.
apk_copy_path = "{0}.copy.apk".format(
os.path.join(
os.path.dirname(apk_path),
os.path.splitext(os.path.basename(apk_path))[0],
)
)

try:
apk_copy_path = shutil.copy2(apk_path, apk_copy_path)

align_cmd = [self.zipalign_path, "-p", "-v", "-f", "4", apk_copy_path, apk_path]

self.logger.info('Running align command "{0}"'.format(" ".join(align_cmd)))
output = subprocess.check_output(
align_cmd, stderr=subprocess.STDOUT
).strip()
return output.decode(errors="replace")
except subprocess.CalledProcessError as e:
self.logger.error(
"Error during align command: {0}".format(
e.output.decode(errors="replace") if e.output else e
)
)
raise
except Exception as e:
self.logger.error("Error during aligning: {0}".format(e))
raise
finally:
# Remove the temp file used for zipalign.
if os.path.isfile(apk_copy_path):
os.remove(apk_copy_path)


class ApkSigner(object):
def __init__(self):
self.logger = logging.getLogger(
"{0}.{1}".format(__name__, self.__class__.__name__)
)

if "JARSIGNER_PATH" in os.environ:
self.jarsigner_path: str = os.environ["JARSIGNER_PATH"]
if "APKSIGNER_PATH" in os.environ:
self.apksigner_path: str = os.environ["APKSIGNER_PATH"]
else:
self.jarsigner_path: str = "jarsigner"
self.apksigner_path: str = "apksigner"

full_jarsigner_path = shutil.which(self.jarsigner_path)
full_apksigner_path = shutil.which(self.apksigner_path)

# Make sure to use the full path of the executable (needed for cross-platform
# compatibility).
if full_jarsigner_path is None:
if full_apksigner_path is None:
raise RuntimeError(
'Something is wrong with executable "{0}"'.format(self.jarsigner_path)
'Something is wrong with executable "{0}"'.format(self.apksigner_path)
)
else:
self.jarsigner_path = full_jarsigner_path
self.apksigner_path = full_apksigner_path

def sign(
self,
Expand All @@ -225,24 +288,19 @@ def sign(
raise FileNotFoundError('Unable to find file "{0}"'.format(apk_path))

sign_cmd: List[str] = [
self.jarsigner_path,
"-tsa",
"http://timestamp.comodoca.com/rfc3161",
"-sigalg",
"SHA1withRSA",
"-digestalg",
"SHA1",
"-keystore",
self.apksigner_path,
"sign",
"--ks",
keystore_file_path,
"-storepass",
keystore_password,
apk_path,
"--ks-key-alias",
key_alias,
"--ks-pass",
f"pass:{keystore_password}",
apk_path,
]

if key_password:
sign_cmd.insert(-2, "-keypass")
sign_cmd.insert(-2, key_password)
sign_cmd.insert(-2, " --key-pass ")
sign_cmd.insert(-2, f"pass:{key_password} ")

try:
self.logger.info('Running sign command "{0}"'.format(" ".join(sign_cmd)))
Expand Down Expand Up @@ -310,66 +368,3 @@ def resign(
return self.sign(
apk_path, keystore_file_path, keystore_password, key_alias, key_password
)


class Zipalign(object):
def __init__(self):
self.logger = logging.getLogger(
"{0}.{1}".format(__name__, self.__class__.__name__)
)

if "ZIPALIGN_PATH" in os.environ:
self.zipalign_path: str = os.environ["ZIPALIGN_PATH"]
else:
self.zipalign_path: str = "zipalign"

full_zipalign_path = shutil.which(self.zipalign_path)

# Make sure to use the full path of the executable (needed for cross-platform
# compatibility).
if full_zipalign_path is None:
raise RuntimeError(
'Something is wrong with executable "{0}"'.format(self.zipalign_path)
)
else:
self.zipalign_path = full_zipalign_path

def align(self, apk_path: str) -> str:

# Check if the apk file to align is a valid file.
if not os.path.isfile(apk_path):
self.logger.error('Unable to find file "{0}"'.format(apk_path))
raise FileNotFoundError('Unable to find file "{0}"'.format(apk_path))

# Since zipalign cannot be run inplace, a temp file will be created.
apk_copy_path = "{0}.copy.apk".format(
os.path.join(
os.path.dirname(apk_path),
os.path.splitext(os.path.basename(apk_path))[0],
)
)

try:
apk_copy_path = shutil.copy2(apk_path, apk_copy_path)

align_cmd = [self.zipalign_path, "-v", "-f", "4", apk_copy_path, apk_path]

self.logger.info('Running align command "{0}"'.format(" ".join(align_cmd)))
output = subprocess.check_output(
align_cmd, stderr=subprocess.STDOUT
).strip()
return output.decode(errors="replace")
except subprocess.CalledProcessError as e:
self.logger.error(
"Error during align command: {0}".format(
e.output.decode(errors="replace") if e.output else e
)
)
raise
except Exception as e:
self.logger.error("Error during aligning: {0}".format(e))
raise
finally:
# Remove the temp file used for zipalign.
if os.path.isfile(apk_copy_path):
os.remove(apk_copy_path)
2 changes: 1 addition & 1 deletion src/test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_valid_basic_command_without_quotes(
# Mock the command line parser.
arguments = cli.get_cmd_args(
"-w {working_dir} -d {destination} "
"-o Rebuild -o NewSignature -o NewAlignment {apk_file}".format(
"-o Rebuild -o NewAlignment -o NewSignature {apk_file}".format(
working_dir=tmp_working_directory_path,
destination=obfuscated_apk_path,
apk_file=tmp_demo_apk_v10_original_path,
Expand Down
2 changes: 1 addition & 1 deletion src/test/test_obfuscation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def test_perform_full_obfuscation_valid_apk(
"Reorder",
"RandomManifest",
"Rebuild",
"NewSignature",
"NewAlignment",
"NewSignature",
],
tmp_working_directory_path,
obfuscated_apk_path,
Expand Down
Loading