mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify native ABI completeness, JNI exports and 16 KiB ELF load alignment."""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import tempfile
|
|
import zipfile
|
|
|
|
|
|
# The native methods each of the app's Java classes declares. ART looks them up
|
|
# by name only when they are first called, so a missing one fails at run time.
|
|
NATIVE_METHODS = {
|
|
"FlectarActivity": (
|
|
"nativeGoogleAuthorizationResult",
|
|
"nativeMicrosoftRedirect",
|
|
"nativeDocumentResult",
|
|
"nativeDocumentsResult",
|
|
"nativeDocumentRequestActive",
|
|
"nativeSuspendPdfPreview",
|
|
"nativeTrimMemory",
|
|
"nativeResumed",
|
|
"nativePaused",
|
|
"nativeNotificationOpened",
|
|
"nativeNotificationPermissionAnswered",
|
|
),
|
|
"GoogleAuthorization": ("nativeResult",),
|
|
"MailCheckJob": ("nativeCheck", "nativeStop"),
|
|
"NotificationActionReceiver": ("nativeRespond",),
|
|
}
|
|
|
|
REQUIRED_EXPORTS = {
|
|
"ANativeActivity_onCreate",
|
|
*(f"Java_com_flectar_mail_{class_name}_{method}"
|
|
for class_name, methods in NATIVE_METHODS.items() for method in methods),
|
|
}
|
|
|
|
|
|
def verify_jni_exports(data, name):
|
|
# Inspect the dynamic symbol table: strings or a debug symbol table do not
|
|
# prove that ART can find these functions after release stripping/LTO.
|
|
readelf = os.environ.get("READELF") or shutil.which("llvm-readelf") or shutil.which("readelf")
|
|
if not readelf:
|
|
raise ValueError("JNI verification requires llvm-readelf or readelf (or set READELF)")
|
|
with tempfile.TemporaryDirectory(prefix="flectar-jni-") as directory:
|
|
library = Path(directory) / "library.so"
|
|
library.write_bytes(data)
|
|
result = subprocess.run(
|
|
[readelf, "--dyn-syms", "--wide", str(library)],
|
|
check=True, capture_output=True, text=True,
|
|
env={**os.environ, "LC_ALL": "C"},
|
|
)
|
|
exported = set()
|
|
for line in result.stdout.splitlines():
|
|
fields = line.split()
|
|
if (len(fields) >= 8 and fields[3] == "FUNC"
|
|
and fields[4] in {"GLOBAL", "WEAK"}
|
|
and fields[5] in {"DEFAULT", "PROTECTED"} and fields[6] != "UND"):
|
|
exported.add(fields[7])
|
|
missing = REQUIRED_EXPORTS - exported
|
|
if missing:
|
|
raise ValueError(f"{name}: missing native exports: {', '.join(sorted(missing))}")
|
|
|
|
|
|
def verify_elf(data, name):
|
|
if data[:6] != b"\x7fELF\x02\x01":
|
|
raise ValueError(f"{name}: expected little-endian ELF64")
|
|
offset = struct.unpack_from("<Q", data, 32)[0]
|
|
entry_size, count = struct.unpack_from("<HH", data, 54)
|
|
if entry_size < 56 or count == 0 or offset + count * entry_size > len(data):
|
|
raise ValueError(f"{name}: invalid ELF program headers")
|
|
loads = 0
|
|
for i in range(count):
|
|
entry = offset + i * entry_size
|
|
if struct.unpack_from("<I", data, entry)[0] != 1:
|
|
continue
|
|
file_offset, address = struct.unpack_from("<QQ", data, entry + 8)
|
|
alignment = struct.unpack_from("<Q", data, entry + 48)[0]
|
|
if alignment < 16384 or file_offset % 16384 != address % 16384:
|
|
raise ValueError(f"{name}: PT_LOAD is not aligned for 16 KiB Android pages")
|
|
loads += 1
|
|
if not loads:
|
|
raise ValueError(f"{name}: missing ELF load segments")
|
|
|
|
|
|
def verify_apk(path):
|
|
with zipfile.ZipFile(path) as archive:
|
|
names = set(archive.namelist())
|
|
app_libraries = sorted(name for name in names if name.endswith("/libflectar_mail_android.so"))
|
|
if not app_libraries:
|
|
raise ValueError("APK contains no Flectar native application")
|
|
for app in app_libraries:
|
|
abi = app.split("/")[1]
|
|
pdfium = f"lib/{abi}/libpdfium.so"
|
|
provenance = f"assets/licenses/PDFium/{abi}/pdfium-licenses/build.json"
|
|
license_name = f"assets/licenses/PDFium/{abi}/pdfium-licenses/LICENSE"
|
|
if not {pdfium, provenance, license_name} <= names:
|
|
raise ValueError(f"{abi}: missing bundled PDFium runtime, provenance or license")
|
|
for name in sorted(names):
|
|
if name.startswith("lib/") and name.endswith(".so"):
|
|
verify_elf(archive.read(name), name)
|
|
for app in app_libraries:
|
|
verify_jni_exports(archive.read(app), app)
|
|
print("PASS: native libraries include JNI exports, PDFium and licenses, with 16 KiB ELF alignment")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("apk")
|
|
verify_apk(parser.parse_args().apk)
|