Changelog¶
All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
Correctness release. Several protections were not doing anything, and nothing in the suite noticed because the assertions checked shape rather than outcome. No configuration changes are required.
Fixed¶
- The pure-Python
zipalignnever worked. Its local-file-header format string unpacked 11 fields into 10 names, so it raisedValueErroron the first entry of every input. Nobody hit it because the tests ran the Android SDK binary whenever one was onPATH— which is the case on developer machines and on GitHub runners, but not forpip install fuinwithout the SDK, the setup the docs advertise. Two further bugs sat behind it: the padding calculation ignored any existing extra field, and the central directory was copied verbatim, leaving every entry offset stale once padding shifted the entries. The aligner is rewritten against the central directory and now rejects ZIP64 archives and already-signed APKs instead of silently corrupting them. - The pure-Python v2 signature was never valid. The APK Signing Block size
fields were 8 bytes too large, so
apksignercould not find the block at all and reported the APK as v2-unsigned; behind that, the signers sequence was missing its outer length prefix, the certificate sequence carried one prefix too many, the additional-attributes sequence declared a zero-length attribute, the content digest was computed per section instead of over one flat chunk list, and the EOCD was digested with its central-directory offset zeroed rather than left pointing at the signing block. Output now verifies withapksigner verify, which the suite asserts. - v1 signing destroyed the alignment
zipalignhad just applied, because it rebuilds the archive throughzipfile. The fallback path re-aligns between v1 and v2. X-Android-APK-Signed: 2is now written into the.SF, so a stripped v2 block cannot silently downgrade verification to v1.exclude_filesdeleted the native libraries it excluded. The encryptor returned a blanket^lib/.*\.so$strip pattern regardless of what it had actually encrypted, so an excluded.sowas neither encrypted nor shipped.- The resource-map chunk type was wrong (
0x00180002, against Android's0x00080180), so fuin parsed zero resource IDs and reportedmin_sdk,target_sdk,version_code,version_nameand permissions as empty for every real APK. The test fixture emitted the same wrong value and so agreed with the bug. - A malformed manifest could exhaust memory.
string_countis an attacker-controlledu32and was used directly as a loop bound, so a few-hundred-byte upload could claim0xFFFFFFFFstrings. It is now bounded by the buffer. Reachable fromPOST /pack. patch_axmlraised on truncated input while formatting the warning that the input was truncated.- The fallback manifest patcher could ship a corrupt manifest. When the
replacement class name differed in length it did a raw byte
replace, which shifts every AXML offset, and still reported success — sostrict_manifest_patchpassed. It now declines and reports failure. - Duplicate ZIP entries were silently collapsed onto the last copy, because
entries were read by name rather than through their
ZipInfo. Signing now rejects duplicate names outright. - A genuine
apksignerfailure was mistaken for a missing JRE — the check searched stderr for"java", which matches every Java stack trace — and quietly fell through to the fallback signer. - A failure to read the signing certificate disabled anti-tamper silently. It is now only tolerated for the generated debug keystore.
- The pack report said "Encrypted DEX files: 0" for single-DEX apps: it
derived the count from removed entries, and
classes.dexcomes back as the stub. axml/no longer opens ZIP files, restoring thepacker→apk→axmllayering the docs describe.get_apk_infomoves tofuin.apk.infoand its error path returns the same keys as its success path.- Encrypted asset names use the full SHA-256 digest instead of a 64-bit prefix, which was cheap to collide and silently dropped one of the colliding assets.
Security¶
- Webhook URLs are validated. The per-request
webhook_urlwas POSTed to unchecked, so any authenticated caller could reach cloud instance metadata or internal hosts. Targets must now behttps(FUIN_WEBHOOK_ALLOW_HTTPopts into plain http) and resolve entirely to public addresses. - Upload limits are enforced while reading.
POST /analyzehad no limit at all andPOST /packchecked only after the whole body was in memory. - The API key is compared with
secrets.compare_digestrather than==. - Job errors no longer return raw exception text, which carried server temp and keystore paths.
- The container runs as a non-root user and applies migrations before serving.
- External build tools run with a timeout, so a wedged
apksignercannot pin a worker forever.
Added¶
GET /health— unauthenticated liveness probe, plus a DockerHEALTHCHECK.- 16 KiB page alignment for uncompressed
lib/**/*.so, for Android 15 devices. Opt in viazipalign(..., so_alignment=PAGE_ALIGNMENT). - Jobs left
runningby a restart are marked failed at startup instead of being reported as in progress forever. - SQLite connections enable WAL, a busy timeout and foreign keys;
FUIN_DATABASE_URLnow works for non-SQLite backends. - An SSE subscriber that connects after a job finished receives the terminal
state instead of blocking forever, and progress events are marshalled onto
the event loop rather than pushed onto an
asyncio.Queuefrom a worker thread.
Changed¶
inject_encrypted_dextakes anInjectedAssetsdataclass instead of fourteen keyword arguments.encrypt_native_libs/encrypt_resourcesreturnEncryptedEntriesinstead of an untypeddict, and share one entry reader.- Entry selection (
is_native_lib,is_user_asset) lives infuin.contract, soanalyzeandpackcannot disagree about what gets encrypted. - New
fuin.apk.zip_formatmodule holds byte-level ZIP record parsing, shared by signing and alignment.
2.0.0 - 2026-08-01¶
A large release. The Python package is reorganised, the web service moves behind an extra, and several latent bugs are fixed. Read the migration notes below before upgrading.
Breaking¶
pip install fuinno longer installs the server. The base install is the packer alone —cryptographyandpython-dotenv, down from 12 packages. Installfuin[server]to getfuin-server.- Module paths changed. The flat package is grouped by concern:
| Before | After |
|---|---|
fuin.crypto |
fuin.encryption.aes |
fuin.string_encrypt |
fuin.encryption.dex_strings |
fuin.native_lib |
fuin.encryption.native_libs |
fuin.resource_encrypt |
fuin.encryption.resources |
fuin.apk |
fuin.apk.repack (or fuin.apk) |
fuin.signing, fuin.zipalign, fuin.keystore, fuin.stub_dex |
fuin.apk.* |
fuin.android_tools |
fuin.apk.tools |
fuin.manifest.patch_manifest |
fuin.apk.patch_manifest |
fuin.manifest (byte level) |
fuin.axml.patcher |
fuin.apk_info |
fuin.axml.info |
fuin.analyze, fuin.report |
fuin.reporting.* |
fuin._constants |
fuin.contract (+ fuin.axml.constants, fuin.apk.constants) |
fuin.integrity |
removed — use fuin.apk.extract_cert_fingerprint |
fuin.server.models |
fuin.server.schemas |
- Gradle composite-build path changed. includeBuild("path/to/fuin/gradle-plugin") |
|
becomes includeBuild("path/to/fuin/jvm/gradle-plugin"). |
|
- run_pipeline returns a PackedOutput instead of a (path, sha256, report) tuple. |
|
- PipelineOptions is removed; use PackOptions. |
|
- The Pydantic PackResult is renamed PackedApp, to stop colliding with the |
|
packer's PackResult dataclass. RegisterAppResponse is removed (unused). |
|
- Protection options are now tri-state. Omitting --root-detection, |
|
--emulator-detection, --encrypt-strings, --verify-signature or |
|
--no-strict-manifest-patch defers to the matching FUIN_* variable; passing |
|
one always wins. Previously an explicit False could be overridden by the |
|
| environment. |
Fixed¶
- The published wheel could not pack.
assets/stub.dexsat outside the package directory and was never included, sopip install fuinhad no stub to inject. It now ships inside the wheel. - The Docker image did not start. The build never installed the project, so
the
fuin-serverconsole script did not exist and the container exited immediately. - Settings were captured at import time, so
FUIN_*changes needed a process restart, and every pipeline test wrote packed APKs into the repository instead of a temp directory. package_namecould be read from the wrong resource ID, returning an unrelated string-pool entry on APKs carrying aversionCode.version_codewas alwaysNone— initialised and never assigned.- Concurrent pack jobs clobbered each other through a single shared
.pending.apkscratch path. pipeline.pyre-listed all 13PackOptionsfields to override one, silently dropping any field added later.- The in-memory job store grew without bound, retaining every finished job's full result for the process lifetime.
- Detached asyncio tasks were created without holding a reference and could be garbage-collected mid-flight.
- A DEX between 44 and 63 bytes raised
struct.errorinstead of being skipped (off-by-one in the header bounds check). reset_enginedropped the engine without disposing its connection pool.- Dead code removed from the AXML patcher, including an authoring note left in a shipped source file.
Added¶
- Documentation site at https://ykus4.github.io/fuin — 14 pages covering
installation, all five interfaces, configuration, the API, architecture,
security and development. Built with
--strictin CI and deployed to GitHub Pages. FUIN_MAX_MAPPING_MBfor the ProGuard mapping upload limit, previously hardcoded at 50 MB.- Tests for
signingandstring_encrypt, neither of which had any coverage. - mypy, running clean over the package, in CI and pre-commit.
- CI now tests Python 3.12, 3.13 and 3.14 (the Docker image ships 3.14 and had never been tested), measures coverage, runs the Docker image rather than only building it, exercises Alembic upgrade/downgrade and checks the migrated schema against the models, and verifies a packer-only wheel install can pack an APK.
Changed¶
- Adopted a src layout (
src/fuin/). stub/andgradle-plugin/are grouped underjvm/.- The two AXML parsers — one in the inspector, one in the patcher — are unified
into
fuin.axml. PKCS12 handling, external-tool invocation and ZIP-copy loops are likewise deduplicated. - The server gains router, repository and settings layers;
main.pydrops from 286 lines to app assembly only. action.ymlno longer installs the dev dependency group, and no longer breaks workspace-relative input/output paths.- README trimmed from 446 lines to an entry point, with the content moved to the documentation site.
1.1.2 - 2026-05-09¶
See the release notes.
1.1.1 - 2026-05-07¶
See the release notes.
1.1.0 - 2026-05-07¶
See the release notes.
1.0.0 - 2026-05-02¶
Initial release.