Skip to content

apk.py

ofrak.core.apk

ApkAnalyzer (Analyzer)

Analyzes Android APK packages using aapt to extract package metadata including package name, application name, version information, SDK requirements, permissions, and launchable activity. The analyzer parses structured output from aapt dump badging to extract key APK attributes useful for understanding app identity, requirements, and capabilities. Use when you need to identify an APK's package name, determine version and SDK requirements, audit requested permissions, or find the main activity without unpacking the entire APK.

analyze(self, resource, config=None) async

Analyze a resource for to extract specific ResourceAttributes.

Users should not call this method directly; rather, they should run Resource.run or Resource.analyze.

Parameters:

Name Type Description Default
resource Resource

The resource that is being analyzed

required
config

Optional config for analyzing. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run.

None

Returns:

Type Description
ApkAttributes

The analysis results

Source code in ofrak/core/apk.py
async def analyze(self, resource: Resource, config=None) -> ApkAttributes:
    async with resource.temp_to_disk(suffix=".apk") as temp_path:
        cmd = ["aapt", "dump", "badging", temp_path]
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()
        output = stdout.decode("utf-8")
        return self._parse_aapt_output(output)

_parse_aapt_output(self, output) private

Parse aapt dump badging output to extract APK attributes.

Parameters:

Name Type Description Default
output str

Raw output from aapt dump badging command

required

Returns:

Type Description
ApkAttributes

Parsed APK attributes

Source code in ofrak/core/apk.py
def _parse_aapt_output(self, output: str) -> ApkAttributes:
    """
    Parse aapt dump badging output to extract APK attributes.

    :param output: Raw output from aapt dump badging command

    :return: Parsed APK attributes
    """
    lines = output.split("\n")

    package_name = None
    version_code = None
    sdk_version = None
    target_sdk_version = None
    application_name = None
    launchable_activity = None
    permissions = []

    for line in lines:
        # Parse package line: package: name='com.example' versionCode='123' ...
        if line.startswith("package:"):
            package_match = re.search(r"name='([^']+)'", line)
            if package_match:
                package_name = package_match.group(1)

            version_code_match = re.search(r"versionCode='([^']+)'", line)
            if version_code_match:
                version_code = int(version_code_match.group(1))

        # Parse sdkVersion line: sdkVersion:'23'
        elif line.startswith("sdkVersion:"):
            sdk_match = re.search(r"sdkVersion:'(\d+)'", line)
            if sdk_match:
                sdk_version = int(sdk_match.group(1))

        # Parse targetSdkVersion line: targetSdkVersion:'30'
        elif line.startswith("targetSdkVersion:"):
            target_match = re.search(r"targetSdkVersion:'(\d+)'", line)
            if target_match:
                target_sdk_version = int(target_match.group(1))

        # Parse application-label line: application-label:'MyApp'
        elif line.startswith("application-label:") and not line.startswith(
            "application-label-"
        ):
            label_match = re.search(r"application-label:'([^']+)'", line)
            if label_match:
                application_name = label_match.group(1)

        # Parse launchable-activity line: launchable-activity: name='com.example.MainActivity' ...
        elif line.startswith("launchable-activity:"):
            activity_match = re.search(r"name='([^']+)'", line)
            if activity_match:
                launchable_activity = activity_match.group(1)

        # Parse uses-permission line: uses-permission: name='android.permission.INTERNET'
        elif line.startswith("uses-permission:"):
            permission_match = re.search(r"name='([^']+)'", line)
            if permission_match:
                permissions.append(permission_match.group(1))

    if package_name is None:
        raise ValueError("Failed to extract package name from aapt output")
    if sdk_version is None:
        raise ValueError("Failed to extract SDK version from aapt output")
    if target_sdk_version is None:
        raise ValueError("Failed to extract target SDK version from aapt output")

    return ApkAttributes(
        package_name=package_name,
        application_name=application_name,
        version_code=version_code,
        sdk_version=sdk_version,
        target_sdk_version=target_sdk_version,
        permissions=permissions,
        launchable_activity=launchable_activity,
    )

ApkAttributes (ResourceAttributes) dataclass

Attributes extracted from an Android APK package using aapt.

Parameters:

Name Type Description Default
package_name

The unique package identifier (e.g., 'com.example.app')

required
application_name

The human-readable application name

required
version_code

Integer version code for internal versioning

required
sdk_version

Minimum SDK version required to run the app

required
target_sdk_version

Target SDK version the app was built for

required
permissions

List of Android permissions requested by the app

required
launchable_activity

Main activity that launches the app

required

ApkIdentifier (Identifier)

Identifier for ApkArchive.

Some Apks are recognized by the MagicMimePattern; others are tagged as JavaArchive or ZipArchive. This identifier inspects those files, and tags any with an androidmanifest.xml as an ApkArchive.

identify(self, resource, config=None) async

Perform identification on the given resource.

Users should not call this method directly; rather, they should run Resource.identify.

Parameters:

Name Type Description Default
resource Resource required
config

Optional config for identifying. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run.

None
Source code in ofrak/core/apk.py
async def identify(self, resource: Resource, config=None) -> None:
    async with resource.temp_to_disk(suffix=".zip") as temp_path:
        unzip_cmd = [
            "unzip",
            "-l",
            temp_path,
        ]
        unzip_proc = await asyncio.create_subprocess_exec(
            *unzip_cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await unzip_proc.communicate()
        if unzip_proc.returncode:
            raise CalledProcessError(returncode=unzip_proc.returncode, cmd=unzip_cmd)

        if b"androidmanifest.xml" in stdout.lower():
            resource.add_tag(Apk)

ApkPacker (Packer)

Repackages decoded Android APK resources into a complete, signed APK file using apktool for compilation and uber-apk-signer for signing. The process recompiles resources, repackages DEX files, updates the manifest, and creates cryptographic signatures required for Android installation. Use after modifying Android app resources, Smali code, or manifest to create an installable APK.

This unpacker is a wrapper for two tools:

Another helpful overview of the process: https://github.com/vaibhavpandeyvpz/apkstudio.

pack(self, resource, config=ApkPackerConfig(sign_apk=True)) async

Pack disassembled APK resources into an APK.

Parameters:

Name Type Description Default
resource Resource required
config ApkPackerConfig ApkPackerConfig(sign_apk=True)
Source code in ofrak/core/apk.py
async def pack(
    self, resource: Resource, config: ApkPackerConfig = ApkPackerConfig(sign_apk=True)
):
    """
    Pack disassembled APK resources into an APK.

    :param resource:
    :param config:
    """
    apk = await resource.view_as(Apk)
    temp_flush_dir = await apk.flush_to_disk()
    apk_suffix = ".apk"
    with tempfile.NamedTemporaryFile(suffix=apk_suffix, delete_on_close=False) as temp_apk:
        temp_apk.close()
        apk_cmd = [
            "apktool",
            "build",
            "--force-all",
            temp_flush_dir,
            "--output",
            temp_apk.name,
        ]
        apk_proc = await asyncio.create_subprocess_exec(
            *apk_cmd,
        )
        apk_returncode = await apk_proc.wait()
        if apk_proc.returncode:
            raise CalledProcessError(returncode=apk_returncode, cmd=apk_cmd)
        if not config.sign_apk:
            # Close the file handle and reopen, to avoid observed situations where temp.read()
            # was not returning data
            with open(temp_apk.name, "rb") as file_handle:
                new_data = file_handle.read()
        else:
            with tempfile.TemporaryDirectory() as signed_apk_temp_dir:
                java_cmd = [
                    "java",
                    "-jar",
                    _UberApkSignerTool.JAR_PATH,
                    "--apks",
                    temp_apk.name,
                    "--out",
                    signed_apk_temp_dir,
                    "--allowResign",
                ]
                java_proc = await asyncio.create_subprocess_exec(
                    *java_cmd,
                )
                java_returncode = await java_proc.wait()
                if java_proc.returncode:
                    raise CalledProcessError(returncode=java_returncode, cmd=java_cmd)
                signed_apk_filename = (
                    os.path.basename(temp_apk.name)[: -len(apk_suffix)]
                    + "-aligned-debugSigned.apk"
                )
                signed_file_name = os.path.join(
                    signed_apk_temp_dir,
                    signed_apk_filename,
                )
                with open(signed_file_name, "rb") as file_handle:
                    new_data = file_handle.read()
        assert len(new_data) != 0
        resource.queue_patch(Range(0, await resource.get_data_length()), new_data)

ApkPackerConfig (ComponentConfig) dataclass

ApkPackerConfig(sign_apk: bool)

ApkUnpacker (Unpacker)

Decodes Android APK application packages into their component files and resources using apktool (see https://ibotpeaches.github.io/Apktool/). This tool decodes the AndroidManifest.xml back to readable XML, extracts resources (images, layouts, strings) in their original format, converts DEX bytecode to Smali assembly, and preserves the complete directory structure. Use when reverse engineering Android applications, analyzing app behavior, examining resource files, or preparing to modify and repackage an APK. The decoded files are much easier to read and modify than the compiled APK format.

unpack(self, resource, config=None) async

Decode Android APK files.

Parameters:

Name Type Description Default
resource Resource required
config None
Source code in ofrak/core/apk.py
async def unpack(self, resource: Resource, config=None):
    """
    Decode Android APK files.

    :param resource:
    :param config:
    """
    apk = await resource.view_as(Apk)
    async with resource.temp_to_disk() as temp_path:
        with tempfile.TemporaryDirectory() as temp_flush_dir:
            cmd = [
                "apktool",
                "decode",
                "--output",
                temp_flush_dir,
                "--force",
                temp_path,
            ]
            proc = await asyncio.create_subprocess_exec(
                *cmd,
            )
            returncode = await proc.wait()
            if proc.returncode:
                raise CalledProcessError(returncode=returncode, cmd=cmd)
            await apk.initialize_from_disk(temp_flush_dir)

_UberApkSignerTool (ComponentExternalTool) private

__init__(self) special

Initialize self. See help(type(self)) for accurate signature.

Source code in ofrak/core/apk.py
def __init__(self):
    super().__init__(
        _UberApkSignerTool.JAR_PATH,
        "https://github.com/patrickfav/uber-apk-signer",
        install_check_arg="",
    )

is_tool_installed(self) async

Check if a tool is installed by running it with the install_check_arg. This method runs <tool> <install_check_arg>.

Returns:

Type Description
bool

True if the tool command returned zero, False if tool could not be found or returned non-zero exit code.

Source code in ofrak/core/apk.py
async def is_tool_installed(self) -> bool:
    if not os.path.exists(_UberApkSignerTool.JAR_PATH):
        return False

    try:
        cmd = [
            "java",
            "-jar",
            _UberApkSignerTool.JAR_PATH,
            "--help",
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
        )
        returncode = await proc.wait()
    except FileNotFoundError:
        return False

    return 0 == returncode