- Every request made with an F-Droid repository's login (entry.jar, index, APKs), redirects included, now uses the scoped check: same origin and inside the repository directory by whole plain path segments. A same-origin redirect elsewhere on the server gets no login. - _child() refuses file names a server could normalise outside the repository: double-encoded dots, ';' path parameters, encoded slashes or backslashes (anything _credentials.path_segments rejects). - Store page: moveLogin() strips the login from the address with plain text matching first, then decodes with a fallback to the text as typed, so '100%done', '%FF' or an address the URL parser rejects can no longer leave the password in the field. Tests: outgoing headers for '%252e%252e/...' and '..;/...' payloads and a same-origin redirect outside the repository carry no Authorization; the page test covers undecodable and unparseable logins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
APK repositories
Frame Control supports F-Droid-format repositories, including F-Droid, F-Droid archive, IzzyOnDroid and user-provided HTTPS repositories. Repository indexes are authenticated before their apps appear. Search lists builds with Android API ≤30 and arm64-v8a or no native libraries, using the same streaming reducer as the existing catalogue. This does not guarantee an app works in Lepton.
Formats considered
| Format | Users and purpose | Support in this source |
|---|---|---|
| F-Droid v2 | F-Droid, IzzyOnDroid, self-hosted fdroidserver repositories; consumed by F-Droid clients including Droid-ify and Neo Store | Preferred: signed entry.jar authenticates entry.json; its SHA-256 authenticates index-v2.json, which supplies APK SHA-256 hashes |
| F-Droid v1 | Older F-Droid servers and clients | Fallback: verify index-v1.jar, then read its signed index-v1.json |
| Obtainium configurations / exports | Obtainium users share app URLs plus source-specific filters and update settings; exports can contain a list of app configuration objects | Not imported here: configurations describe how to find releases, not one signed repository index |
| SideQuest listings / custom feeds | SideQuest's own app discovery and installation service | No interoperable signed custom-repository specification was established from the public project documentation examined; SideQuest needs its own adapter |
| GitHub release lists | Developers publish APK assets on release pages; community lists link to projects | Not a repository standard: asset naming, build selection and publisher verification vary; handled separately from this F-Droid source |
| Minimal JSON list | A private list could contain package, title, APK URL and SHA-256 | Deliberately not introduced: unsigned hashes downloaded alongside files do not authenticate their publisher; another bespoke signing/update protocol would duplicate F-Droid |
Research references (checked 2026-09-28):
- F-Droid APIs and repository setup.
- F-Droid signing keys and IzzyOnDroid's repository page and fingerprint.
- Droid-ify and Neo Store.
- Obtainium, its configuration/deep-link format, and community app configurations.
- SideQuest's public client. The absence of a specification in these materials is not proof that no historical or private custom-feed format exists.
Add a repository in Frame Control
From the Frame Control checkout, use its source-management CLI:
python3 ui/apk_sources/fdroid.py add 'https://example.org/fdroid/repo?fingerprint=YOUR_64_HEX_CERTIFICATE_FINGERPRINT' --name 'My apps'
python3 ui/apk_sources/fdroid.py list
python3 ui/apk_sources/fdroid.py search SOURCE_ID 'music'
python3 ui/apk_sources/fdroid.py download SOURCE_ID org.example.app
python3 ui/apk_sources/fdroid.py remove SOURCE_ID
Replace SOURCE_ID with the id printed by add or list. --fingerprint
can also supply the pin. fdroidrepos://example.org/fdroid/repo?fingerprint=…
links are accepted and converted to HTTPS. Conflicting fingerprints are refused.
A URL must identify the repository directory, not its website or an index file.
Adding fetches and validates the complete index before saving the source. Without a fingerprint, Frame Control verifies the JAR signature and remembers its signer: trust on first use (TOFU). This establishes continuity with the first server response, not independent publisher identity. Obtain the published fingerprint through a trusted channel when possible; the store's Add a source form shows the pinned one ("Trusted on first use: …") so you can compare it. Re-adding an existing URL preserves its pin; changing it requires deliberately removing and re-adding it.
Repositories behind a login
Some self-hosted repositories sit behind HTTP basic auth. As in F-Droid's own
client, the login can be part of the address
(https://user:pass@example.org/fdroid/repo), or given separately: in the
store's Add a source form under "This repository needs a login", in the
server API (username and password beside url), or on the command line,
where the password is read from standard input so it stays out of shell history
and ps:
printf '%s\n' "$PASSWORD" | python3 ui/apk_sources/fdroid.py add 'https://example.org/fdroid/repo' --username me --password-stdin
How the login is kept and used:
- Kept out of the address. The
user:pass@part is removed before anything is saved, so the saved repository URL, its id, the source list, job results and the page never contain it. The store only says "Uses a login". - Stored separately. Logins live in
frame_host.data_dir('source-credentials.json')(~/Library/Application Support/Frame Control/source-credentials.jsonon macOS), written with mode 0600 and keyed by source id. They are stored in plain text there, protected by file permissions only (on Windows, the folder's normal ACLs). Removing the repository deletes its login, and the file goes when the last login is removed. Each change holds a lock file (source-credentials.lock), so the app and the command line can't undo each other's saves or removals. - HTTPS only. An
http://address with a login is refused before any request. - Inside the repository only. The
Authorizationheader is decided separately for every request, redirects included: index,entry.jar, APKs and the store's artwork proxy. It is sent only when the request's scheme, host and port match the repository's and its path is inside the repository's directory by whole path segments. A redirect to another host or port, or to another path on the same server, gets no login, and that request's ownAuthorizationheader is removed. Paths with.or..segments (plain or percent-encoded), encoded/or\, double encoding or;get no login, since a server could normalise them to a path outside the repository. Index file names like that are refused outright. - Never echoed. Logins are not written to logs, error messages, API responses or the cache. The CLI prints only the saved source.
- Checked before saving. As with any repository, the whole index must download and verify before anything is saved. HTTP 401 (or 403 when a login was sent) stops the add with "check the username and password" and saves nothing: no repository, no login, no rollback state. A 403 with a login is not treated as rate limiting.
- Same verification. The login changes only who may download. Signature, fingerprint pin or TOFU, index hash and rollback checks apply exactly as for any other repository.
Re-adding the same address with a new login replaces the saved one. Re-adding it without a login keeps the saved one.
The API for the search/server integration is in ui/apk_sources/fdroid.py:
add_repo(url, fingerprint=None, name=None, username=None, password=None), remove_repo(source_id),
set_enabled(source_id, enabled), and user_repos(). The module also exposes
sources, search, details, and download from the shared source contract.
This change supplies the CLI and API; the integrated source-management UI is
separate work. Built-in sources can be disabled but cannot be removed.
Settings and pins live in frame_host.data_dir('apk-repos.json')
(~/Library/Application Support/Frame Control/apk-repos.json on macOS).
Authenticated reduced indexes and APKs live under
frame_host.cache_dir('apk-sources'); indexes refresh after 24 hours. An
expired index is still served (marked stale in the store) while it refreshes in
the background; a failed refresh is retried after 10 minutes.
Only the running Frame Control app prunes cached APKs (at start and after store
downloads); the command-line tools never do.
The existing catalogue's unverified index cache is never treated as authenticated.
Rollback protection: each repository's newest accepted signed index timestamp
is kept in apk-repo-state.json next to the settings, and an older index is
refused. Once a repository has served a v2 entry.jar, a missing entry.jar
is an error rather than a reason to fall back to index-v1.jar. entry.jar
must be signed with SHA-2 (SHA-1 is still accepted for legacy index-v1.jar).
Removing a repository clears its state.
Publish your own repository
Only publish free APKs you own or have the developer's permission to distribute. Do not publish paid app mirrors or bypass store licences. Check distribution terms before adding someone else's repository; this module does not infer legal permission from a signature or automatically audit a repository's terms.
Install a current fdroidserver and its documented Android/Java dependencies on the publishing machine, then:
mkdir my-fdroid
cd my-fdroid
fdroid init
# Set repo_url in config.yml to https://example.org/fdroid/repo
# Also set repo_name and repo_description; keep the generated signing key safe.
cp /path/to/your-free-app.apk repo/
fdroid update --create-metadata
# Review the generated metadata (name, summary, licence, source and website).
fdroid update
Serve the generated repo directory at that HTTPS URL, including APKs,
icons, entry.jar, index-v2.json and index-v1.jar. Do not publish the
private signing keystore or configuration passwords. Configure fdroidserver's
serverwebroot and run fdroid deploy for managed publication, or copy the
public directory with your existing deployment tool. Publish the SHA-256
repository certificate fingerprint displayed by fdroidserver in a link such as
https://example.org/fdroid/repo?fingerprint=….
Keep the repository signing key backed up: changing it breaks existing pins.
For updates, add the new APK, edit metadata as needed, run fdroid update and
publish again. Test the published URL with Frame Control's add, search and
download commands. The above publisher setup is documented from fdroidserver;
it was not executed as part of this implementation.
Verification and limits
The stdlib verifier supports one RSA PKCS#1 v1.5 JAR/CMS signer with a key of
2048–8192 bits; SHA-256/384/512 and legacy SHA-1 digest encodings are
recognized. It checks the signer certificate pin, the signature over .SF,
the whole-manifest digest, and the manifest's digest of the JSON member.
ECDSA, DSA, RSA-PSS, multiple signers and section-only .SF manifests are
rejected. Certificates are pinned identities, not validated as Web PKI chains.
HTTPS certificates are separately checked by Python's normal TLS validation.
v1 fallback occurs only when entry.jar returns HTTP 404 or 410. Signature,
fingerprint, index hash, TLS and server errors never trigger an unsigned
fallback. APKs are cached by SHA-256 and checked again before reuse. Here,
verified: true means the bytes match the signed repository's APK hash; it
is not an independent APK publisher-signature or runtime compatibility verdict.
There is no repository timestamp rollback/expiry policy or automated signing-key
rotation yet. An old correctly signed index can still validate.
Offline fixtures exercise v2, v1, TOFU, pin changes, disabled sources, cache
reuse, URL rejection and corruption of every signature/hash layer. On the Mac,
the real IzzyOnDroid repository was added with its published pin, searched for
Tiny Music Player, and its 16,520-byte APK downloaded with SHA-256
d7bcb24d101b04beb3394b695b24be4e2c3d6ed702f1d0e06bc4dd707f64d86a.
No headset connection or installation was performed.