From 7b1a1656f74c961cd15b8786b9356b4221a0446c Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 10 Aug 2026 19:43:28 +0200 Subject: [PATCH] Back up what adb can actually reach, and block YouTube in Firefox too phone_backup.sh captures package lists (including what was purged for user 0), every third-party APK with its splits, /sdcard, all three settings namespaces, and the focus-mode state. Verified: 54/54 packages, 126 APK files, 5.1 GB, splits intact. It is deliberately not sold as a full-device backup, because on an unrooted phone it cannot be one: /data/data is unreachable, so logins, in-app data and device pairings do not survive. It writes a RESTORE-BY-HAND.md naming each of those, with a restore order that avoids a lockout -- mObywatel last, because activating it on a new device deactivates the old one. The first two runs pulled 1 of 54 APKs and then 6 of 54: `while read < file` with adb inside the loop lets adb drain the same stdin. Reading into an array first fixes it, and verify_backup now compares package count against the manifest rather than only checking that the files it did pull are valid zips -- integrity alone called the 1-of-54 run a pass. Also documents the uBlock Origin filters, which block YouTube in Firefox independently of RethinkDNS: force-stopping the VPN still leaves youtube.com blocked by ||youtube.com^. And records what this is worth -- Play offers a one-tap Install for YouTube, the VPN is a few taps from off, and the uBlock page has a Proceed button. Three independent layers, none of them tamper-proof; only Device Owner removes the off switch. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JxNrcMDcizNB3XrrYpZF23 --- .../phone-backup-and-ublock-2026-08-09.json | 48 +++ .../docs/youtube-block-unrooted.md | 62 ++++ phone_focus_mode/phone_backup.sh | 333 ++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 docs/superpowers/evidence/phone-backup-and-ublock-2026-08-09.json create mode 100755 phone_focus_mode/phone_backup.sh diff --git a/docs/superpowers/evidence/phone-backup-and-ublock-2026-08-09.json b/docs/superpowers/evidence/phone-backup-and-ublock-2026-08-09.json new file mode 100644 index 00000000..8d837ee7 --- /dev/null +++ b/docs/superpowers/evidence/phone-backup-and-ublock-2026-08-09.json @@ -0,0 +1,48 @@ +{ + "intent": "Add a second, VPN-independent YouTube block in Firefox, and give the phone a backup script covering everything ADB can actually reach before a possible Device Owner factory reset.", + "scope": [ + "phone_focus_mode/phone_backup.sh", + "phone_focus_mode/docs/youtube-block-unrooted.md", + "Non-goal: app private data -- unreachable without root, documented rather than faked", + "Non-goal: tamper-resistance; only Device Owner removes the off switch" + ], + "changes": [ + "phone_backup.sh captures package lists (including what was purged for user 0), every third-party APK with its splits, /sdcard user data, all three settings namespaces, and the focus-mode state. It ships a RESTORE-BY-HAND.md naming what it cannot restore, and --verify re-reads the artefacts.", + "Documented the uBlock Origin layer: the 8 filters, the import procedure, and why `adb shell input text` must not be used to enter them.", + "Documented measured undo costs for all three layers, including the Play Store finding, so the doc does not overstate the block." + ], + "verification": [ + { + "command": "./phone_backup.sh --out ~/phone-backup", + "result": "pass", + "evidence": "54/54 packages, 126 APK files, 5.1 GB, 'Verify OK'. Split APKs captured in full (com.termux has base + 5 split_config). Spot-checked 3 random APKs with unzip -t: all OK. pl.infakt.infakt, pl.mbank, com.kunzisoft.keepass.libre and org.thoughtcrime.securesms all present." + }, + { + "command": "force-stop com.celzero.bravedns, then load youtube.com in Firefox", + "result": "pass", + "evidence": "'uBlock Origin has prevented the following page from loading: https://m.youtube.com/ ... filter ||youtube.com^ ... found in: My filters'. The layers are independent: killing the VPN does not restore YouTube. Control example.com still loads; VPN restored afterwards." + }, + { + "command": "market://details?id=com.google.android.youtube", + "result": "fail", + "evidence": "Google Play shows a live Install button, so the app-removal layer is a ONE-TAP undo -- weaker than the VPN toggle. Not installed; state re-confirmed installed=false. Recorded in the doc rather than left implicit." + }, + { + "command": "shellcheck + shfmt on phone_backup.sh", + "result": "pass", + "evidence": "Clean." + } + ], + "risks": [ + "The first two runs of phone_backup.sh pulled 1 of 54 and then 6 of 54 APKs. Cause: `while read < file` with `adb` inside the loop -- adb inherits and drains the same stdin. Fixed by reading into an array with mapfile first; the same bug was fixed in the .kdbx loop. verify_backup now compares package-directory count against packages-third-party.txt, because an integrity-only check called the 1-of-54 run a pass.", + "Backgrounding the script with nohup truncated it mid-run twice; it must be run in the foreground. Not a script defect but worth knowing before trusting an unattended run.", + "No .kdbx is captured: KeePassDX keeps its database in private storage. Confirmed with the user that the canonical copy lives on the PC, so this is expected here -- downgraded from a warning to an info line, which means a genuinely missing database on some other device would be quieter.", + "infakt (pl.infakt.infakt) is device-paired for banking ('Usun powiazanie z telefonem') and holds a live session with funds and an active card. The account is server-side so login works on a new device, but the pairing and app PIN do not survive a wipe; re-pairing is via SMS to the number registered in-app. 2FA method (SMS vs authenticator) still unconfirmed by the user.", + "uBlock filters live in the Firefox profile and are NOT in the backup; they must be re-added by hand after a wipe." + ], + "rollback": [ + "git revert removes the script and doc changes; no device state is touched by reverting.", + "Delete a backup directory under ~/phone-backup/ to reclaim space -- each full run is ~5 GB.", + "Remove the uBlock filters from uBlock dashboard -> My filters and press apply." + ] +} diff --git a/phone_focus_mode/docs/youtube-block-unrooted.md b/phone_focus_mode/docs/youtube-block-unrooted.md index 9d88453a..af05192b 100644 --- a/phone_focus_mode/docs/youtube-block-unrooted.md +++ b/phone_focus_mode/docs/youtube-block-unrooted.md @@ -40,6 +40,42 @@ than trusting the `pm` exit code, and exits non-zero if anything is still installed — so a Play Store reinstall surfaces as a failure instead of a silent no-op. +## Layer 2: uBlock Origin in Firefox + +Independent of the VPN, and that is the point: stopping RethinkDNS does +**not** restore YouTube in Firefox. Verified by force-stopping +`com.celzero.bravedns` and loading youtube.com, which produced *"uBlock +Origin has prevented the following page from loading — `||youtube.com^` — +found in: My filters"*. + +The filters, in uBlock's dashboard → **My filters**: + +``` +! YouTube block +||youtube.com^ +||youtu.be^ +||googlevideo.com^ +||ytimg.com^ +||youtube-nocookie.com^ +||ggpht.com^ +||yt3.ggpht.com^ +||m.youtube.com^ +``` + +Add them by writing that list to a file, pushing it to +`/sdcard/Download/`, triggering a media scan (`am broadcast -a +android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/...`, +or the picker will not see it), then using uBlock's **import** button. +Do NOT type them with `adb shell input text`: it silently strips `|` and +`^`, leaving bare domains that do not block. Press the checkmark to +apply, and confirm it greys out. + +These live in the Firefox profile, so they are **not** captured by +`phone_backup.sh` and must be re-added by hand after a wipe. + +The block page has a **Proceed** button — one tap. This layer raises the +cost of bypassing; it does not remove the bypass. + ## What is NOT blocked **Firefox (`org.mozilla.fenix`) is still installed and still reaches @@ -76,6 +112,32 @@ PC off and does not fail closed on network loss. Not yet scoped; the VPN slot was going to be checked when the device disconnected. Note Android allows only one active VPN at a time, so this would conflict with any real VPN. +## How strong this actually is + +Measured, not assumed: + +| Layer | Undo cost | +| --- | --- | +| App removal | **One tap.** Google Play shows a live **Install** button for YouTube (checked on device 2026-08-09; not installed). | +| RethinkDNS rules | A few taps — the VPN toggle in Settings or the quick-settings tile. | +| uBlock filters | **Proceed** on the block page, or disabling the extension. | + +Nothing here is tamper-resistant, and nothing here can be: on an +unrooted device without Device Owner, no app can prevent the user +disabling a VPN or reinstalling from Play. The layers are independent — +undoing one does not undo the others — so the cost is "three separate +places" rather than one. That is the honest ceiling. + +`DISALLOW_CONFIG_VPN` plus `setAlwaysOnVpnPackage(..., lockdown)` under +Device Owner is the only tier that removes the off switch, and it needs +the factory reset in `docs/device-owner-wipe-checklist.md`. See also the +FLAG_SYSTEM note above: `focus_owner` would need fixing before Device +Owner could touch YouTube at all. + +Also note nothing re-asserts the purge automatically. +`phone-auto-sync.timer` is inactive and its `ExecStart` points at a path +that does not exist, so `distraction_purge.sh` is manual today. + ## Redoing this after a factory reset The purge is per-user state and does not survive a wipe. Re-run diff --git a/phone_focus_mode/phone_backup.sh b/phone_focus_mode/phone_backup.sh new file mode 100755 index 00000000..8a770b64 --- /dev/null +++ b/phone_focus_mode/phone_backup.sh @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +# ============================================================ +# Phone backup — everything ADB can reach on an UNROOTED device +# +# READ THIS FIRST. This is NOT a full-device backup and cannot +# be one. Restoring from it does NOT give you the phone back as +# it was; it gives you your FILES back and a checklist of what +# to reinstall and re-authenticate by hand. +# +# What it captures: +# - every installed package name, split into system/third-party +# - the APK of every third-party app (so you can reinstall +# offline, including F-Droid apps that may have moved on) +# - /sdcard user data: DCIM, Pictures, Download, Documents, +# Movies, Music, plus Signal backups if present +# - settings (system/secure/global), which covers most of the +# phone's own configuration +# - the focus-mode state: which packages are purged, and the +# RethinkDNS domain rules if its export is present +# +# What it CANNOT capture, on any unrooted phone: +# - app private data (/data/data/): logins, sessions, +# in-app settings, message history, game saves +# - anything hardware-bound: bank device pairings, infakt's +# phone pairing, mObywatel activation, Signal registration +# - the Firefox profile (tabs, cookies, extension settings) +# +# Reading another app's private storage needs root or a +# debuggable build. `adb backup` is deprecated and a no-op for +# most apps since Android 12. That is a platform limit, not a +# gap in this script -- so it prints, at the end, an explicit +# list of what you will have to redo by hand. +# +# Usage: +# ./phone_backup.sh [--out DIR] [--no-apks] [--verify DIR] +# ============================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +# shellcheck source=lib/adb_common.sh +source "${SCRIPT_DIR}/lib/adb_common.sh" + +OUT_ROOT="${HOME}/phone-backup" +PULL_APKS=1 +VERIFY_DIR="" + +# Directories pulled wholesale from shared storage. Anything not +# listed here is not backed up, so keep the list honest. +readonly SDCARD_DIRS=( + DCIM + Pictures + Download + Documents + Movies + Music + Signal + RunnerUp +) + +usage() { + sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --out) + OUT_ROOT="${2:?--out needs a directory}" + shift 2 + ;; + --no-apks) + PULL_APKS=0 + shift + ;; + --verify) + VERIFY_DIR="${2:?--verify needs a directory}" + shift 2 + ;; + -h | --help) usage ;; + *) + _error "Unknown flag: $1" + exit 1 + ;; + esac +done + +# A backup nobody checked is not a backup. --verify re-reads the +# artefacts and fails loudly on anything empty or missing. +verify_backup() { + local dir="${1:?}" failed=0 f="" + _info "Verifying ${dir}" + + for f in packages-all.txt packages-third-party.txt settings-secure.txt manifest.txt; do + if [[ ! -s "${dir}/${f}" ]]; then + _error "missing or empty: ${f}" + failed=1 + fi + done + + if [[ -d "${dir}/apks" ]]; then + local apk_count=0 broken=0 pkg_dirs=0 expected=0 + while IFS= read -r -d '' apk; do + apk_count=$((apk_count + 1)) + # An APK is a zip; a truncated pull is the failure mode. + unzip -t "${apk}" >/dev/null 2>&1 || { + _error "corrupt APK: $(basename "${apk}")" + broken=1 + } + done < <(find "${dir}/apks" -name '*.apk' -print0 2>/dev/null) + + # Coverage, not just integrity: the stdin bug produced ONE + # perfectly valid APK out of 54, which an integrity-only check + # would have called a pass. + pkg_dirs="$(find "${dir}/apks" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)" + expected="$(wc -l <"${dir}/packages-third-party.txt")" + _info "APKs: ${apk_count} files across ${pkg_dirs}/${expected} packages" + if [[ "${pkg_dirs}" -lt "${expected}" ]]; then + _error "only ${pkg_dirs} of ${expected} packages have APKs" + failed=1 + fi + [[ "${broken}" -eq 1 ]] && failed=1 + fi + + local kdbx_count=0 + kdbx_count="$(find "${dir}/sdcard" -iname '*.kdbx' 2>/dev/null | wc -l)" + if [[ "${kdbx_count}" -eq 0 ]]; then + # Expected on this phone: the database lives in KeePassDX's + # private storage and the canonical copy is on the PC. Noted + # rather than warned so a real regression stays visible. + _info "No .kdbx in shared storage (canonical copy is on the PC)" + else + _info "KeePassDX databases: ${kdbx_count}" + fi + + if [[ "${failed}" -eq 1 ]]; then + _error "VERIFY FAILED" + return 1 + fi + _info "Verify OK" +} + +dump_packages() { + local dir="$1" + adb_cmd shell pm list packages | sed 's/^package://' | tr -d '\r' | + sort >"${dir}/packages-all.txt" + adb_cmd shell pm list packages -3 | sed 's/^package://' | tr -d '\r' | + sort >"${dir}/packages-third-party.txt" + # Packages removed for user 0 read as uninstalled here; recorded + # separately so a restore knows what was deliberately purged. + adb_cmd shell pm list packages -u | sed 's/^package://' | tr -d '\r' | + sort >"${dir}/packages-including-removed.txt" + comm -13 "${dir}/packages-all.txt" "${dir}/packages-including-removed.txt" \ + >"${dir}/packages-removed-for-user0.txt" + _info "Packages: $(wc -l <"${dir}/packages-third-party.txt") third-party, $(wc -l <"${dir}/packages-removed-for-user0.txt") removed for user 0" +} + +pull_apks() { + local dir="$1" pkg="" path="" n=0 base="" + local -a pkgs=() paths=() + mkdir -p "${dir}/apks" + + # Read the list into an array FIRST. Iterating the file with + # `while read < file` breaks after one package, because `adb` + # inside the loop inherits and drains that same stdin -- measured: + # it pulled 1 of 54 before this was fixed. + mapfile -t pkgs <"${dir}/packages-third-party.txt" + + for pkg in "${pkgs[@]}"; do + [[ -n "${pkg}" ]] || continue + # A split app reports several paths (base + split_config.*). + # All of them are needed for a working offline reinstall, so + # each package gets its own directory rather than one file. + mapfile -t paths < <( + adb_cmd shell pm path "${pkg}" 2>/dev/null | + sed 's/^package://' | tr -d '\r' + ) + [[ "${#paths[@]}" -gt 0 ]] || { + _warn "no APK path for ${pkg}" + continue + } + mkdir -p "${dir}/apks/${pkg}" + for path in "${paths[@]}"; do + [[ -n "${path}" ]] || continue + base="$(basename "${path}")" + adb_cmd pull "${path}" "${dir}/apks/${pkg}/${base}" >/dev/null 2>&1 || + _warn "could not pull ${base} for ${pkg}" + done + n=$((n + 1)) + done + _info "APKs pulled: ${n} packages, $(find "${dir}/apks" -name '*.apk' | wc -l) files" +} + +pull_sdcard() { + local dir="$1" d="" + mkdir -p "${dir}/sdcard" + for d in "${SDCARD_DIRS[@]}"; do + if adb_cmd shell "test -d /sdcard/${d}" 2>/dev/null; then + _info "Pulling /sdcard/${d}" + adb_cmd pull "/sdcard/${d}" "${dir}/sdcard/" >/dev/null 2>&1 || + _warn "partial or failed pull: ${d}" + fi + done + # Same stdin caveat as pull_apks: collect first, pull second. + local -a kdbx_paths=() + mapfile -t kdbx_paths < <( + adb_cmd shell 'find /sdcard -iname "*.kdbx" 2>/dev/null' | tr -d '\r' + ) + local kdbx="" + for kdbx in "${kdbx_paths[@]}"; do + [[ -n "${kdbx}" ]] || continue + _info "Pulling KeePass database: ${kdbx}" + mkdir -p "${dir}/sdcard/keepass" + adb_cmd pull "${kdbx}" "${dir}/sdcard/keepass/" >/dev/null 2>&1 || true + done +} + +dump_settings() { + local dir="$1" ns="" + for ns in system secure global; do + adb_cmd shell settings list "${ns}" | tr -d '\r' | sort \ + >"${dir}/settings-${ns}.txt" + done + _info "Settings captured (system/secure/global)" +} + +dump_focus_state() { + local dir="$1" + { + printf '# Focus-mode state at backup time\n' + printf '# Restore with: phone_focus_mode/distraction_purge.sh\n\n' + printf '## Packages removed for user 0\n' + cat "${dir}/packages-removed-for-user0.txt" + printf '\n## RethinkDNS installed: ' + grep -qx 'com.celzero.bravedns' "${dir}/packages-all.txt" && + printf 'yes\n' || printf 'no\n' + printf '## uBlock filters live in the Firefox profile and CANNOT be\n' + printf '## backed up from adb; see docs/youtube-block-unrooted.md\n' + } >"${dir}/focus-state.md" +} + +write_manifest() { + local dir="$1" + { + printf 'Backup of %s (%s)\n' "${DEVICE_SERIAL}" "${DEVICE_MODEL}" + printf 'Fingerprint: %s\n' "${DEVICE_FINGERPRINT}" + printf 'Taken: %s\n' "$(date -Is)" + printf 'Android: %s (SDK %s)\n' \ + "$(adb_cmd shell getprop ro.build.version.release | tr -d '\r')" \ + "$(adb_cmd shell getprop ro.build.version.sdk | tr -d '\r')" + printf '\nContents:\n' + du -sh "${dir}"/* 2>/dev/null | sed 's/^/ /' + } >"${dir}/manifest.txt" +} + +print_manual_checklist() { + local dir="$1" + cat >"${dir}/RESTORE-BY-HAND.md" <<'EOF' +# What this backup does NOT restore + +Everything below is app-private or hardware-bound, so it is unreachable +from adb on an unrooted phone. Budget real time for it after a wipe. + +## Must be re-authenticated (device pairing / re-activation) + +- **infakt** (`pl.infakt.infakt`) — the account itself is server-side, so + login + password works on a new device. BUT its banking side is + device-paired ("Usuń powiązanie z telefonem" in Ustawienia bankowości) + and the app PIN is local. Expect to re-pair via SMS to the registered + number. Confirm 2FA method BEFORE wiping: SMS is safe, an authenticator + app means the seed must be in KeePassDX. +- **mBank** (`pl.mbank`) — re-pair the device, SMS code, new app PIN. +- **Revolut** — re-login, SMS/email, possibly selfie re-verification. +- **mObywatel** (`pl.nask.mobywatel`) — full re-activation via Profil + Zaufany or a bank login. Do this LAST: activating on a new device + deactivates the old one, with no rollback. +- **Signal** — needs its own backup + the 30-digit passphrase. The + restore prompt appears at install time ONLY. + +## Lost unless separately exported + +- Firefox: tabs, cookies, logins, and the uBlock Origin filter list +- App settings and in-app data for every third-party app +- Game/app progress not synced to a server + +## Restore order that avoids a lockout + +1. Google account (needs backup codes if 2FA is on a device you wiped) +2. KeePassDX, from the `.kdbx` in `sdcard/keepass/` +3. Signal, at install time +4. Banks (mBank, Revolut, infakt) +5. mObywatel last +6. Re-apply focus mode: `distraction_purge.sh`, reinstall RethinkDNS and + re-add its domain rules, re-add the uBlock filters +EOF + _info "Wrote ${dir}/RESTORE-BY-HAND.md -- read it before relying on this backup" +} + +main() { + if [[ -n "${VERIFY_DIR}" ]]; then + verify_backup "${VERIFY_DIR}" + return + fi + + adb_select_device + adb_verify_trusted_identity + + local stamp="" dir="" + stamp="$(date +%Y-%m-%d-%H%M%S)" + dir="${OUT_ROOT}/${DEVICE_SERIAL}-${stamp}" + mkdir -p "${dir}" + + _box "PHONE BACKUP" \ + "Device: ${DEVICE_MODEL} (${DEVICE_SERIAL})" \ + "Output: ${dir}" \ + "This does NOT capture app private data -- see RESTORE-BY-HAND.md" + + dump_packages "${dir}" + [[ "${PULL_APKS}" -eq 1 ]] && pull_apks "${dir}" + pull_sdcard "${dir}" + dump_settings "${dir}" + dump_focus_state "${dir}" + print_manual_checklist "${dir}" + write_manifest "${dir}" + + verify_backup "${dir}" + + _info "Backup complete: ${dir}" + _info "Total size: $(du -sh "${dir}" | cut -f1)" +} + +main "$@"