Files
flamenet-messenger-android/scripts/publish-fdroid.sh
T
Paul HittandClaude Opus 4.8 141f3168a5 Stop publishing the build machine's paths with the F-Droid repo
`fdroid update` writes a status/ directory containing update.json and
running.json, which record the absolute paths of the machine that built the
repo — home directory, username, toolchain locations — and that signing
happened there. My publish script rsynced the whole repo directory, so those
went public and were being served unauthenticated.

That is targeting data for a supply-chain attack against the laptop holding the
Android release key, which is permanent and cannot be rotated without orphaning
every install. fdroid's own deploy command excludes status/; a raw rsync does
not, and I used a raw rsync.

Removed from the live repo and excluded here so it does not come back.

Found by an audit pass, not by anything going wrong — which is the argument for
running them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 07:38:44 -04:00

83 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Publish a built release APK into the Flamenet F-Droid repository.
#
# 🔴 **This is OUR OWN repository, not a submission to f-droid.org.** The
# difference is not cosmetic: f-droid.org rebuilds from source and signs with
# THEIR key, so an app already installed under our signature could not be
# upgraded by it — Android refuses a differently-signed APK over an existing
# install. This repo indexes the APK we already signed, so it upgrades cleanly
# for everybody who sideloaded from the website.
#
# 🔴 **The repo working directory holds a signing keystore and its password.**
# It lives OUTSIDE every git checkout on purpose — all four repos are public.
# Never move it in, and never redirect (`>`) over config.yml: that file holds
# the only on-disk copy of the keystore password, and this project has already
# clobbered a credentials file that way once.
#
# ⚠️ The repo index key is permanent. Replacing it makes every client that has
# added this repository reject its updates, silently, until they remove and
# re-add it. Back up ~/flamenet-fdroid off this machine.
#
# Usage: scripts/publish-fdroid.sh [path/to/app-release.apk]
set -euo pipefail
FDROID_DIR="${FDROID_DIR:-$HOME/flamenet-fdroid}"
FDROID_BIN="${FDROID_BIN:-}"
APK="${1:-app/build/outputs/apk/release/app-release.apk}"
REMOTE="${FDROID_REMOTE:-ovh}"
REMOTE_DIR="${FDROID_REMOTE_DIR:-/srv/prod/messenger/fdroid}"
[ -f "$APK" ] || { echo "no APK at $APK — build it first" >&2; exit 1; }
[ -d "$FDROID_DIR" ] || { echo "no repo at $FDROID_DIR" >&2; exit 1; }
[ -f "$FDROID_DIR/keystore.p12" ] || {
echo "no keystore in $FDROID_DIR — refusing to re-init over a live repo" >&2; exit 1; }
if [ -z "$FDROID_BIN" ]; then
echo "set FDROID_BIN to the fdroid executable (a venv with fdroidserver)" >&2
exit 1
fi
export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@21}"
export PATH="$JAVA_HOME/bin:$PATH"
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
# Name the file by its real version, read out of the APK rather than typed —
# same rule as the update manifest.
aapt2="$(find "$ANDROID_HOME/build-tools" -maxdepth 2 -name aapt2 -type f | sort -V | tail -1)"
badging="$("$aapt2" dump badging "$APK")"
name="$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<<"$badging" | head -1)"
[ -n "$name" ] || { echo "could not read versionName from $APK" >&2; exit 1; }
echo "publishing $APK as version $name"
cp "$APK" "$FDROID_DIR/repo/flamenet-messenger-$name.apk"
cd "$FDROID_DIR"
# `update` re-reads every APK in repo/, rebuilds the index and signs it. Older
# releases move to archive/ per `archive_older` rather than being deleted, so a
# version somebody is pinned to does not vanish.
"$FDROID_BIN" update --pretty
echo "syncing to $REMOTE:$REMOTE_DIR"
# 🔴 Exclude fdroidserver's own status/ directory. It writes update.json and
# running.json containing the ABSOLUTE PATHS of the build machine — the
# maintainer's home directory, username, toolchain locations — and the fact
# that signing happens on that machine. Published, that is targeting data for
# a supply-chain attack on the laptop holding the permanent Android release
# key. fdroid's own `deploy` command excludes it; a raw rsync does not, so it
# went public until an audit caught it.
rsync -az --delete --exclude 'status/' -e ssh repo/ "$REMOTE:/tmp/fdroid-repo-new/"
[ -d archive ] && rsync -az --delete --exclude 'status/' -e ssh archive/ "$REMOTE:/tmp/fdroid-archive-new/" || true
# shellcheck disable=SC2029
ssh "$REMOTE" "set -e
sudo rm -rf $REMOTE_DIR/repo && sudo mkdir -p $REMOTE_DIR
sudo cp -r /tmp/fdroid-repo-new $REMOTE_DIR/repo && rm -rf /tmp/fdroid-repo-new
if [ -d /tmp/fdroid-archive-new ]; then
sudo rm -rf $REMOTE_DIR/archive
sudo cp -r /tmp/fdroid-archive-new $REMOTE_DIR/archive && rm -rf /tmp/fdroid-archive-new
fi
sudo chown -R root:root $REMOTE_DIR && sudo chmod -R a+rX $REMOTE_DIR"
echo
echo "published. verify it is actually being served:"
echo " curl -sS https://flamenetmessenger.com/fdroid/repo/index-v2.json | head -c 200"