Files
skills-public/android-backup-restore.md

563 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: android-backup-restore
description: >-
Use this skill when extracting, backing up, migrating, or restoring data from an Android
device — especially before a wipe, ROM flash, factory reset, or device-to-device move, and
when deciding what is salvageable at a given access level. Covers the full ladder of access
tiers (EDL/firehose & bootloader, custom recovery CWM/TWRP/OrangeFox, root, device-owner,
Shizuku, plain ADB, and on-device apps), file-based-encryption decryption limits, per-partition
dumps (userdata, EFS/modem/IMEI, persist), SELinux/UID-correct restores, the death of
`adb backup`, and app-specific data (WhatsApp, Signal, TOTP/Keystore) that does not survive.
---
# Android Backup & Restore — Extraction by Access Level
How to pull as much as possible off an Android device before wiping, flashing, or migrating —
and how to put it back. Organised as a **ladder of access tiers**: the more privileged the
access, the more you can extract. Work out which tier you have, pull from the highest tier
available, and fall back down the ladder for whatever that tier cannot reach.
This is a *data-recovery and migration* skill for devices you own or are authorised to service.
Shell orchestration here follows [`bash.md`](skills-public/code-languages/bash.md) (strict mode,
hard `timeout`s, no wait-loops); CLI invocation follows
[`cli-tooling.md`](skills-public/cli-tooling.md) (read `<tool> --help` before trusting any flag
below — tool surfaces drift, and the exact partition/block paths are **per-device**).
> [!IMPORTANT]
> **The single biggest variable is encryption.** Every modern Android device uses File-Based
> Encryption (FBE); the `/data` partition is a ciphertext blob until something that holds the
> user credential derives the keys. That "something" is normally the booted OS. A custom
> recovery can only read plaintext `/data` if it was *built to decrypt your exact Android
> version* and you type the lockscreen PIN/password into it. See §3. This is why "boot it
> normally and pull over ADB/root" beats "boot to recovery and dd" whenever the device still
> boots.
---
## 1. The Access Ladder (decision table)
Pick the highest row you can reach. Each tier can extract everything below it plus its own column.
| Tier | Access you need | Reaches raw partitions? | Reaches decrypted `/data/data` app data? | Reaches EFS/IMEI/persist? | Notes |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **EDL / firehose / BROM** (§8) | Vendor edl/mtk loader, device in 9008/BROM | ✅ full flash, offline | ⚠️ only as ciphertext (still FBE-locked) | ✅ | Last resort for dead/bricked devices; dumps are encrypted blobs unless you also have the keys. |
| **Bootloader / fastboot** (§8) | Unlocked bootloader | ⚠️ some (`fastboot fetch` on newer; mostly flash-only) | ❌ | partial | Mostly a *flashing* interface, not a dumping one. |
| **Custom recovery** TWRP/OrangeFox/CWM (§3) | Unlocked bootloader + matching recovery | ✅ every partition via `dd`/built-in backup | ⚠️ **only if recovery decrypts your FBE** + you enter the PIN | ✅ | Classic "nandroid". Decryption is the catch on Android 10+. |
| **Root (booted OS)** (§4) | Magisk/KernelSU/APatch on a booting device | ✅ (block devices readable) | ✅ **fully decrypted** (OS is running) | ✅ | The best tier for a *working* device — plaintext data + raw partitions together. |
| **Device Owner / DPC** (§5) | `dpm set-device-owner` on a fresh/unprovisioned device | ❌ | ❌ (own managed scope only) | ❌ | Niche; grants/denies permissions, reads some providers; not a bulk dumper. |
| **Shizuku / ADB-shell privilege** (§6) | Shizuku (wireless-debug or one-time ADB) | ❌ | ⚠️ APKs, `Android/data`, `Android/obb`, media — **not** `/data/data` of release apps | ❌ | `adb shell` UID (2000) reach, on-device, no PC tether. |
| **Plain ADB** (§6) | USB debugging authorised | ❌ | ⚠️ same as Shizuku + `run-as` for *debuggable* apps only | ❌ | `adb backup` is effectively dead (§7). Pull storage, providers, bugreport. |
| **On-device app, no root** (§6.4) | Just the unlocked phone | ❌ | ❌ (own data only) | ❌ | Vendor cloud/D2D, Seedvault, per-app export. Last-mile for the non-technical case. |
**Rule of thumb for a pre-wipe sweep:** if it boots and you can unlock it → get **root** (§4) if
possible, else **ADB** (§6). If it does *not* boot → **custom recovery** (§3), else **EDL** (§8).
Always grab **EFS/IMEI/persist** (§4.3) before any flash regardless of tier.
---
## 2. First: inventory what is actually worth saving
Before dumping 128 GB of userdata, enumerate. Most of the value is small and specific.
- **Media & documents** — `/sdcard` (a.k.a. `/storage/emulated/0`): DCIM, Pictures, Download,
Documents, Music, ringtones. Reachable at *every* tier including plain ADB. Pull this first; it
is the easy win and the bulk of what users actually miss.
- **App data** — `/data/data/<pkg>` and `/data/user/0/<pkg>` (credential-encrypted, "CE") and
`/data/user_de/0/<pkg>` (device-encrypted, "DE"). Needs root or recovery-decrypt. This is where
game saves, app logins, notes, 2FA seeds live.
- **Scoped external app data** — `Android/data/<pkg>` and `Android/obb/<pkg>` on `/sdcard`.
Blocked from ordinary file managers since Android 11/13 but reachable via ADB/Shizuku/root.
- **Telephony/PIM** — SMS/MMS, call log, contacts, calendar. See §6.2 (providers) and §9.
- **Identity partitions** — EFS/modem (`modemst1`, `modemst2`, `fsg`), `persist`, `efs`. IMEI,
Wi-Fi/BT MAC, sensor calibration. §4.3. **Irreplaceable — back up before any flash.**
- **The things that will NOT survive** no matter the tier — hardware-backed Keystore keys, and
by extension many 2FA/banking apps. §10. Plan migrations for these *before* wiping.
---
## 3. Custom Recovery (TWRP / OrangeFox / CWM) — the offline nandroid
Use when the device will not boot normally, or you want a block-level image independent of the OS.
Requires an **unlocked bootloader** (itself a wipe on most devices — so unlock *before* you have
data worth keeping, or accept the loss). Boot without flashing when you can:
`fastboot boot twrp.img` leaves the installed recovery untouched.
> [!TIP]
> **Try the *installed* recovery before downloading or temp-booting one.** `adb reboot recovery`,
> then `adb shell id` — many custom ROMs ship a current OrangeFox/TWRP whose `adbd` already runs as
> **root** and, crucially, is new enough to **decrypt FBE on the installed Android version** (so
> `/data/data` shows real package names and readable file contents). This beats `fastboot boot`-ing
> an old recovery image: a recovery built from a much older branch often won't boot on a new ROM at
> all (its kernel panics and the device falls back to the OS — you'll land back in Android as
> `uid=2000(shell)`, the tell-tale that it didn't take). Confirm decryption with
> `ls /data/data` (real names, not hex) and by reading one `*/shared_prefs/*.xml`.
### 3.1 The decryption problem (read this before trusting a backup)
TWRP/OrangeFox can only back up **plaintext** `/data` if its decryption code matches your device's
FBE implementation *and* you enter the lockscreen credential in recovery (Keymaster derives the
keys from it). Reality on Android 10+:
- Use a recovery **built from the branch matching your Android version** (e.g. an `android-12.1`+
build for Android 12+). A recovery built from an old branch shows `/data` as garbage folder names
or fails to mount.
- Devices that **shipped on Android ≤11 and were upgraded** to 12+ often cannot be decrypted by any
TWRP build — the keystore/Keymaster path changed. OrangeFox sometimes succeeds where TWRP fails;
try both. If neither decrypts, you can still image the partition but only as ciphertext (useless
without the keys).
- If decryption fails, **do not factory-reset to "fix" it** — that destroys the keys and the data.
Verify decryption worked before relying on it: in the TWRP terminal, `ls -la /data/data` should
show real package names (`com.whatsapp`, …), not hex blobs.
### 3.2 Built-in backup vs. raw `dd`
- **Built-in backup** (TWRP "Backup" / OrangeFox): tick System, Data, Boot, EFS, etc. Produces a
nandroid set under `/sdcard/TWRP/BACKUPS/`. If `/data` decrypted, the Data archive holds plaintext
app data; if not, it backs up the raw (encrypted) partition. Pull the whole folder off afterwards.
- **Raw partition image** when you want a specific partition or the built-in backup won't cover it.
From the recovery `adb shell` or terminal — **block paths are per-device**, find them first:
```bash
# from recovery shell — enumerate by name, DO NOT guess the numbers
ls -l /dev/block/by-name/ # symlinks: userdata, boot_a, modemst1, efs, persist, …
cat /proc/partitions
# image a named partition to portable storage (not back onto /data)
timeout 1800 dd if=/dev/block/by-name/userdata of=/sdcard/userdata.img bs=8M
```
Pull with `adb pull /sdcard/userdata.img` (or write to an SD card / OTG drive). A raw `userdata`
image is only restorable/mountable if you also have the FBE keys — prefer the decrypted built-in
Data backup for app data, and reserve raw images for small partitions (§4.3).
### 3.3 Restore
Restore the built-in nandroid through the recovery UI, or `dd` a raw image back **only onto the
exact same partition of the same device** (never another device's EFS — §4.3). After a decrypted
Data restore across Android versions, expect some apps to misbehave; a clean reinstall + selective
data restore (§4.2) is often more reliable than a whole-partition nandroid across versions.
---
## 4. Root on a booting device — the best tier for a live phone
If the device boots and you can unlock it, root (Magisk / KernelSU / APatch) is the strongest
position: the OS has already derived the FBE keys, so **all app data is plaintext**, and you still
have raw block-device access. (For installing root on this workstation's own test devices, Magisk
APKs/modules may already be on the NAS — see
[check-nas-before-downloading](check-nas-before-downloading.md) before fetching.)
### 4.1 Bulk app-data pull
```bash
# as root, on device or via `adb shell su -c`
# tar preserves owners/modes/symlinks — plain `cp`/`adb pull` loses them (see §4.4)
su -c 'tar -cpf /sdcard/appdata-whatsapp.tar \
/data/data/com.whatsapp /data/user_de/0/com.whatsapp'
# then from the PC:
adb pull /sdcard/appdata-whatsapp.tar
```
Pull `/data/data/<pkg>` (CE) **and** `/data/user_de/0/<pkg>` (DE) for each app — some apps split
state across both. For a whole-device grab, `tar` `/data/data`, `/data/user/0`, `/data/user_de/0`
and `/data/misc` (wifi configs, etc.), but mind disk budget on the pulling host.
> [!TIP]
> **Stream straight off the device with `adb exec-out` — no on-device staging.** When `/data` has
> little free space (you can't write a multi-GB tar back onto the partition you're reading), pipe
> the archive directly to the host, and `dd` images the same way (`exec-out` is binary-safe; plain
> `adb shell` mangles bytes):
> ```bash
> adb exec-out 'cd /data && tar -cp -f - data user_de system misc vendor adb' > data-full.tar
> adb exec-out 'dd if=/dev/block/by-name/boot status=none' > boot.img
> ```
> Toybox/busybox `tar` can't store SELinux contexts and exits non-zero when it skips sockets
> (`unknown file type '140000'` — harmless). Capture contexts separately as a restore sidecar:
> `for d in /data/data/* /data/user_de/0/*; do stat -c '%a %U %G %C %n' "$d"; done`. The per-app
> SELinux *categories* (`:c123,c456`) encode the app sandbox and are **not** regenerable by
> `restorecon` alone — this sidecar is how you put them back.
>
> **Record restore metadata while you have the device.** A `dumpsys package` capture gives, per
> app: `codePath` (→ system/vendor/product/`/data/app`=user), `pkgFlags` (SYSTEM/UPDATED_SYSTEM_APP),
> `installerPackageName` (Play `com.android.vending` vs sideloaded `null`), versionCode, and granted
> runtime permissions; `cmd appops dump` adds special ops (SYSTEM_ALERT_WINDOW,
> MANAGE_EXTERNAL_STORAGE, …). Parse these into a manifest so you can later restore *only* user apps
> and re-grant their exact permissions.
Prefer dedicated tools over hand-rolled tar when restoring to a *different* device or Android
version, because they record ownership/SELinux/permissions that a naive tar restore drops (§4.4):
- **Swift Backup** (root; Shizuku for a weaker subset) — app + data, permissions, SSAID.
- **Neo Backup** (FOSS, OAndBackupX fork; root incl. KernelSU/APatch) — app + data, schedules.
- **OAndBackupX / Migrate** — FOSS nandroid-style app+data sets.
### 4.2 Raw partitions while rooted
Same `dd` from `/dev/block/by-name/` as §3.2, but from the running OS (`su -c dd …`). Good for
grabbing `boot`/`vendor_boot` to patch, or EFS/persist (§4.3) without rebooting to recovery.
### 4.3 EFS / modem / IMEI / persist — back these up before ANY flash
These hold IMEI, baseband calibration, Wi-Fi/BT MAC, and sensor calibration. Corrupting them bricks
the radio permanently and there is no download that restores *your* unique values. **Partition names
and block numbers are per-device — look them up, never copy numbers from a guide.**
```bash
su -c 'ls -l /dev/block/by-name/' | grep -Ei 'efs|modemst|fsg|fsc|persist|nvram|nvdata|protect'
# Qualcomm: commonly modemst1, modemst2, fsg, fsc, persist
# MediaTek: commonly nvram, nvdata, protect_f, protect_s, persist
for p in modemst1 modemst2 fsg persist; do
su -c "dd if=/dev/block/by-name/$p of=/sdcard/efs-$p.img bs=4M"
done
```
Pull the images **off the device and keep copies on a host/NAS** — a backup that lives only on the
partition you are about to wipe is no backup. Restore the same way (`dd … of=/dev/block/by-name/$p`)
**only to the identical partition on the identical device**. Never flash one phone's EFS onto
another — you would overwrite its unique identity with useless values.
### 4.4 Restoring raw `/data/data` tars correctly (UID + SELinux)
A tar of `/data/data/<pkg>` restored by hand will *not* launch unless you fix two things Android
assigns at install time:
1. **Install the app first** (same package, ideally same version) so Android creates the data dir
with the correct per-app UID and SELinux context.
2. Restore the files, then **fix ownership and labels**:
```bash
# find the app's real UID from the freshly-created dir, don't assume 100xx
su -c 'ls -ld /data/data/com.example' # e.g. u0_a142 u0_a142
su -c 'tar -xpf /sdcard/appdata-example.tar -C /'
su -c 'chown -R u0_a142:u0_a142 /data/data/com.example'
su -c 'restorecon -RFD /data/data/com.example' # argument order matters; -v to see work
```
Then reboot before launching. Skipping the UID chown or `restorecon` is the usual reason a
"successful" restore produces an app that crashes on open. Dedicated backup apps (§4.1) do this
bookkeeping for you — hand-tar only when you must.
### 4.5 Getting root headlessly (no GUI taps) when the device boots
If the bootloader is unlocked and you need root to reach §4 but can't tap through the Magisk app,
patch and flash boot entirely from the shell:
1. **Get the *exact* stock boot.img.** The authoritative copy is the live partition — dump it from
a root recovery (§3) with `dd`/`exec-out`. Don't flash a boot.img of unknown origin: check its
cmdline first (`androidboot.hardware=qcom` = Qualcomm vs MediaTek's `bootopt=…` — a wrong-SoC
image bricks the device).
2. **Patch it** with Magisk's own tooling. The Magisk APK is a zip: its `assets/boot_patch.sh` needs
the arm64 libs renamed (`lib/arm64-v8a/libmagiskboot.so`→`magiskboot`, `libmagiskinit.so`→
`magiskinit`, `libmagisk.so`→`magisk`, `libinit-ld.so`→`init-ld`) plus `stub.apk` and
`util_functions.sh` in the same dir, then `sh boot_patch.sh boot.img` → `new-boot.img`.
- Set the flags **explicitly** (standalone `boot_patch.sh` treats unset as false, which can
bootloop): for a modern FBE device with a vbmeta partition and dynamic partitions use
`KEEPVERITY=true KEEPFORCEENCRYPT=true PATCHVBMETAFLAG=false RECOVERYMODE=false LEGACYSAR=false`.
`Failed to patch` on the kernel DTB step is benign (nothing to patch).
- The recovery installer (`META-INF/com/google/android/update-binary`, which auto-detects these
flags via `get_flags()`) is cleaner **but** fails outside a real sideload: it tries to mount
`/system`, which on super/dynamic-partition ROMs is `/dev/mapper/system` and won't mount in a
plain `adb shell`. `boot_patch.sh` with explicit flags sidesteps that.
3. **Flash** `new-boot.img` (verify it unpacks: `magiskboot unpack new-boot.img`) with
`fastboot flash boot` or, from a root recovery, `dd … of=/dev/block/by-name/boot`. Keep the stock
image for instant rollback. Reboot: `magiskd` in `ps -A` + `magisk -V` confirms root took and
survives reboot.
4. **Grant su to the ADB shell** (Magisk denies it by default, silently): Magisk app → Superuser
access = "Apps and ADB" + Automatic response = "Grant", or seed a `policies` row
(`uid=2000 policy=2`) in `/data/adb/magisk.db`. Until then `adb shell su -c …` returns
`Permission denied` even though root is installed and working.
---
## 5. Device Owner (DPC) — narrow, not a bulk extractor
A device-owner app (set with `adb shell dpm set-device-owner <pkg>/<receiver>` on a device with
**no accounts added yet**) gets elevated *policy* powers, not filesystem powers. It is relevant to
extraction only in that it can silently grant its own runtime permissions and read some content
providers that would otherwise prompt — useful for a managed enrolment flow, not for cleaning out a
phone before a wipe. Hard limits:
- `setPermissionGrantState()` for **sensor** permissions (location, camera, mic, body sensors,
activity recognition) is restricted/deprecated for *profile* owners on Android 12+ (silently
returns false); device owners retain more but the surface is shrinking each release.
- It cannot read other apps' `/data/data`, cannot image partitions, and cannot reach EFS.
If you find yourself reaching for device-owner to extract data, the real answer is almost always
root (§4) or ADB providers (§6.2) instead.
---
## 6. ADB & Shizuku — no root, device boots
Plain ADB runs as the `shell` user (UID 2000). Shizuku grants that **same** privilege to on-device
apps (started via wireless debugging or a one-time ADB command), so their reach is identical to ADB
— handy when you have no PC.
### 6.1 External storage & scoped app dirs
```bash
adb pull /sdcard/ ./sdcard-backup/ # DCIM, Download, Documents, etc. — works everywhere
# Android/data and Android/obb are blocked from on-device file managers (11/13+) but
# the adb shell UID can still read them:
adb pull /sdcard/Android/data/ ./android-data/
adb pull /sdcard/Android/obb/ ./android-obb/
```
### 6.2 Content providers (telephony/PIM without root)
`adb shell content query` reaches providers the `shell` UID is allowed to read. What actually works
on a stock device is uneven:
```bash
adb shell content query --uri content://contacts/phones # contacts — usually readable
adb shell content query --uri content://com.android.calendar/events
# SMS/MMS and call_log generally FAIL for the shell UID (need READ_SMS / be default app):
adb shell content query --uri content://sms # typically permission denied
```
For SMS/MMS, call logs, and contacts as restorable files, use an on-device FOSS app rather than
fighting providers over ADB:
- **SMS Import / Export** (`com.github.tmo1.sms_ie`, F-Droid) — SMS+MMS+call log+contacts to
JSON/ZIP, no root, and it *restores* too (set it as default SMS app during restore).
- **Open Android Backup** (mrrfv) — PC-driven, rootless, uses a companion app (not `adb backup`):
restores apps-without-data, internal storage, and contacts (vCard); exports SMS (CSV) and call
log (txt) as **view-only** (no restore, no MMS attachments).
- Contacts also export cleanly from the stock Contacts app ("Export to .vcf") or via Google account
sync — often the least-effort path for the non-technical case.
### 6.3 `run-as` — app data for *debuggable* apps only
```bash
adb shell run-as com.example.debug tar -cpf - ./ > example-data.tar
```
`run-as` only works for apps with `android:debuggable="true"` (i.e. your own dev builds, not Play
Store releases). Shizuku-based file managers use the same `run-as` path and inherit the same limit.
Release-app `/data/data` is **not** reachable at this tier — that needs root (§4) or a
recovery-decrypt (§3).
### 6.4 Bugreport & app lists (cheap, always worth grabbing)
```bash
adb bugreport ./bugreport.zip # logs, settings dump, installed-package metadata
adb shell pm list packages -3 -f # third-party packages + APK paths, to reinstall later
adb shell cmd package list packages --show-versioncode
```
To re-pull the APKs themselves for reinstall:
```bash
adb shell pm path com.example # -> package:/data/app/.../base.apk + splits
adb pull /data/app/~~.../com.example-.../base.apk # repeat for each split; reinstall with
adb install-multiple base.apk split_config.*.apk # install-multiple
```
### 6.5 Vendor D2D and cloud (no-tech last mile)
- **Google** cloud backup + Pixel/Android cable "device-to-device" restore at setup. App data rides
along *only* if the Google **account** is transferred (Block Store is account-bound); untick
nothing under "Accounts". `allowBackup="false"` stops cloud backup but D2D still transfers app data
unless the app sets `dataExtractionRules`/`android:fullBackupOnly` to opt out.
- **Samsung Smart Switch** (PC or cable): backs up most app data, but **encrypted app data (e.g.
WhatsApp), banking logins, and sideloaded APKs do not transfer** — handle those per §9/§10.
- **Seedvault** (CalyxOS/GrapheneOS/LineageOS): the FOSS backup transport. On Android 15 builds its
**device-to-device** mode (Expert settings) is default-on and captures far more app data than the
old cloud-style transport, because it presents as a D2D migration to each app.
---
## 7. The death of `adb backup` (don't build a plan on it)
`adb backup` was deprecated in Android 10 and gutted in Android 12 (API 31): app data is excluded by
default and only included if the app is **debuggable**, so on essentially all release apps across
Android 12–16 `adb backup` produces an archive with no app data. Treat it as unavailable:
- Don't rely on it for a migration. Use root tooling (§4.1), on-device FOSS exporters (§6.2), or
vendor/Seedvault D2D (§6.5) instead.
- `adb restore` likewise cannot restore encrypted backups and is a dead end for modern data.
- The `bmgr`/LocalTransport backup-manager plumbing still exists for *developers testing their own
backup agents* (`adb shell bmgr …`), but it is a per-app developer tool, not a device-wide
extractor — out of scope for a pre-wipe sweep.
---
## 8. EDL / firehose / BROM — dead or bootloader-locked devices
The lowest level, for devices that will not boot to Android *or* recovery. These talk to the SoC's
emergency loader below the OS:
- **Qualcomm EDL (9008)** — [`bkerler/edl`](https://github.com/bkerler/edl) with a firehose
programmer (`.mbn`/`.elf`) matching the SoC. `edl printgpt` lists partitions; `edl rl dumps
--skip=userdata --genxml` dumps all partitions (minus the huge userdata) to a folder with a
`rawprogram` XML; `edl r <partition> <out.img>` grabs one. Newer SoCs need `--loader=<file>`
because loader autodetection no longer works.
- **MediaTek BROM** — [`bkerler/mtkclient`](https://github.com/bkerler/mtkclient). `./mtk printgpt`,
`./mtk rl out` dumps all partitions to `out/`, `./mtk r <partition> <out.img>` for one; `--preloader`
for BROM mode.
The hard limit: **these dumps are ciphertext.** EDL reads the raw flash below the OS, so `userdata`
comes out FBE-encrypted and is not decryptable without the keys (which live in secure hardware keyed
by the lockscreen credential). EDL is therefore excellent for **small identity partitions**
(EFS/persist/nvram — §4.3), full-flash forensic images, and un-bricking, but it is **not** a way to
read a locked phone's app data. Treat downloaded dumps/tools as untrusted input: keep each in its own
directory, pass paths as arguments, and run any helper scripts from outside the dump directory.
---
## 9. App-specific data that needs its own handling
Some high-value data lives behind app-level encryption and ignores the tier ladder:
- **WhatsApp** — local backup at `/sdcard/WhatsApp/Databases/msgstore.db.crypt15` (+ `/Media`). If
the user enabled **end-to-end encrypted backups**, decryption needs the **64-digit key** (or the
`encrypted_backup.key` file from `/data/data/com.whatsapp/files/`, root-only). Tools:
[`wa-crypt-tools`](https://pypi.org/project/wa-crypt-tools/). Easiest migration is WhatsApp's own
built-in transfer or Google-Drive backup with the same phone number — pull the `crypt15` + media
as insurance, not as the primary path.
- **Signal** — does **not** sync to any cloud. Migrate with Signal's in-app device-transfer, or its
encrypted local backup (Settings → Chats → Backups) whose **30-digit passphrase you must record** —
there is no recovery without it.
- **Authenticator / 2FA** — Google Authenticator exports via its QR "export accounts" flow;
**Aegis** (FOSS) exports an encrypted JSON vault. Do this *before* wiping; TOTP seeds not exported
are gone.
---
## 10. What will NOT survive a wipe (plan around it)
No tier recovers these; the only mitigation is to migrate them *before* the wipe:
- **Hardware-backed Keystore keys** are non-exportable by design — key material never leaves the
TEE/StrongBox. Any app that pinned a device-bound key (banking apps, some password managers,
FIDO/passkeys stored in hardware) will treat the restored/new device as untrusted and re-enrol.
- **Play Integrity / SafetyNet attestation** fails on rooted or custom-ROM devices independently of
your data — so a perfectly-restored banking app may still refuse to run. That is an attestation
problem, not a backup problem; don't chase it as data loss.
- **SSAID / `ANDROID_ID`** changes on wipe (per app-signing-key + user), which can log apps out or
reset entitlements even when their `/data/data` is restored byte-perfect.
Before wiping: export 2FA (§9), confirm password-manager vaults sync to their cloud, record Signal
(§9) and WhatsApp keys, and re-enrol passkeys/banking on the new device rather than expecting a
restore to carry them.
---
## 11. Pre-wipe checklist (the one-screen version)
1. **Decide the tier** (§1). Booting + unlockable → root (§4) or ADB (§6). Not booting → recovery
(§3) → EDL (§8).
2. **EFS/modem/persist first** (§4.3) — before any unlock or flash, off-device.
3. **Media & docs** — `adb pull /sdcard` (§6.1).
4. **App data** — root tar + dedicated tool (§4.1), or recovery-decrypted Data backup (§3).
5. **Scoped dirs** — `Android/data`, `Android/obb` (§6.1).
6. **PIM** — SMS/call-log/contacts via SMS Import-Export or vCard (§6.2).
7. **App-specific** — WhatsApp/Signal/2FA via their own flows (§9).
8. **App list + APKs** — `pm list packages -3`, pull splits (§6.4) to rebuild the app set.
9. **System state the app tars miss** (§12.1) — keystore DB, SSAIDs, URI grants, doze whitelist,
app-ops, launcher/shortcuts/QS/controls, Wi-Fi, SD-card UUIDs, root grants, old `packages.list`.
10. **Baseline + app-sweep** the old ROM (`measure.sh`, `test-all-apps.sh`, §12.4) so the new one
has something to be compared against.
11. **Passwords apps keep themselves** — App Manager's KeyStore password, backup-app passphrases,
Signal/WhatsApp keys (§9). Write them down *before* the wipe; none are recoverable after.
12. **Verify the backups off-device** (open the archive, check `ls /data/data` really decrypted)
*before* you type `fastboot -w` or confirm the reset. A backup you have not read back is not a
backup.
---
## 12. Restoring onto a new ROM (root) — what a data tar does not carry
Learned from a full LineageOS-based A16 → Infinity-X A17 switch (Redmi Note 7, Magisk). Restoring
`/data/data` + `user_de` + `Android/data` per §4.4 brings back most apps, but **every app gets a new
UID** on the new ROM, and a lot of state is keyed by UID or lives in system files. The scripts in
[`scripts/android/`](skills-public/scripts/android/) automate the parts below; they take the
device from `ANDROID_SERIAL` like `adb` itself.
### 12.1 Grab these too, before the wipe (all root, all small)
| What | Where | Why it matters on restore |
| :--- | :--- | :--- |
| Old UID map | `/data/system/packages.list` | Remap everything keyed by UID (SSAID, keystore, grants) |
| Keystore2 DB | `/data/misc/keystore/persistent.sqlite` | Apps whose keys vanish show "Keystore error"/log out (§12.3) |
| SSAIDs | `/data/system/users/0/settings_ssaid.xml` | `ANDROID_ID` change logs apps out (GitHub, Bolt, Lovense, …) |
| Persistent SAF grants | `/data/system/urigrants.xml` | Folder pickers (music players, camera, sync apps) lose their folders |
| Doze whitelist | `/data/system/deviceidle.xml` | Notification/automation apps silently stop in background |
| App-ops & special access | `cmd appops dump`, `cmd notification` policy access, `dumpsys package` | `MANAGE_EXTERNAL_STORAGE`, usage stats, DND, `READ_LOGS`, install-packages, notification listeners are **not** runtime permissions |
| Launcher + pinned shortcuts | launcher DB, `/data/system_ce/0/shortcut_service/` | Home screen; pinned shortcuts (e.g. Ice Box) are owned by the system, not the launcher |
| QS tiles, device controls | `settings get secure sysui_qs_tiles`; SystemUI `user_de` `files/controls_favorites.xml` + `shared_prefs/controls_prefs.xml` | Home Assistant controls, dev tiles |
| Wi-Fi | `/data/misc/apex/com.android.wifi/` or `cmd wifi list-networks` + PSKs | Also note which SSIDs must **not** auto-join |
| Root grants + Magisk settings | `magisk --sqlite "SELECT * FROM policies"`, `… FROM settings`, `… FROM denylist`, `/data/adb/modules` (note which are disabled) | UIDs change, grants don't follow; Zygisk off ⇒ DenyList silently does nothing |
| Accounts | `dumpsys account`; `/data/system_ce/0/accounts_ce.db` | Shows which logins (2nd Google account, app-registered accounts) must be re-added by hand — they can't be transplanted |
| SD-card volume IDs | `blkid` of each SD partition | Public volume ID = filesystem UUID; reformat ⇒ new ID ⇒ broken paths and grants |
| Freezer state | `pm list packages -d` + the freezer app's own list | Updates/reinstalls unfreeze |
### 12.2 Restore order and traps
- **Update first, restore data second.** Installing the old APKs and then the store's update is
fine; restoring data *into* an older APK than the data was written by is a downgrade — apps wipe
or crash on it. Never re-run a full restore pass over updated apps; restore data only.
- **Installer of record:** `pm set-installer` refuses even root. Reinstall the same APKs with
`adb install-multiple -r -i <installer>` (keeps data) so Play/Aurora/F-Droid/Obtainium see their
apps as updatable (`fix-installers.py`).
- **New signature, same package** (forks such as Shizuku → Shizuku-Next for a newer Android): tar the
data dirs, uninstall, install, untar, chown/chcon, re-grant, re-freeze — `reinstall-keep-data.sh`.
- **System files are read only at `system_server` start.** Write `urigrants.xml`,
`settings_ssaid.xml`, `shortcut_service/*` etc. (owner `system:system`, mode 600,
`u:object_r:system_data_file:s0`), then `kill -9 $(pidof system_server)` — a soft restart that
does **not** save over them first, which a normal reboot does. It locks the user: unlock with
wake → swipe → `input text <PIN>` → ENTER (the bouncer screenshots black; retry if needed).
- **fs-verity:** files under `shortcut_service` are verity-protected — `rm` then write, never edit.
- **Restored caches can kill an app:** Ice Box OOM-crashed on a restored MMKV package cache. Drop
`cache`, `code_cache`, WebView caches and per-app package-list caches; keep the real state.
- **Re-map UID-keyed state** with the old/new `packages.list` (SSAID `name="<uid>"`, keystore
`namespace`, Magisk `policies.uid`, doze lists are by package and need no remap).
- **SD card:** if the partition had to be recreated, set the old UUID back
(`tune2fs -U <old>` for ext4, `fsck` before and after) rather than fixing every app.
- **Wi-Fi:** restored networks auto-join in their own order; re-add the ones that must stay
manual with `cmd wifi add-network <ssid> wpa2 <psk> -d`.
- **QS tiles** on A17: `settings put secure sysui_qs_tiles` is ignored; use
`cmd statusbar set-tiles "<list>"`. Developer tiles must be enabled first
(`pm enable 'com.android.settings/.development.qstile.DevelopmentTiles$SensorsOff'`).
- **Launcher switch** (Nova had no build for the new Android): converting Nova's `favorites`
table into Lawnchair's Launcher3 schema works (`nova-to-lawnchair.py`) — mark widgets
`restored=1` so Launcher3 binds fresh widget IDs, and re-pin deep shortcuts for the new launcher.
**Banking-app root checks** are not a restore problem, but they show up right after one. Order
that worked (2026-10, Magisk 31, A17): first the ROM's own spoofing settings, if it has any (PIF,
TrickyStore target and keybox, hide developer status). If an app still crashes on purpose, look for
an Xposed module that defuses that app's check. For PayPal 10.18 that was oslokrok on Vector, the
LSPosed successor. Configure Vector headlessly with
`/data/adb/modules/zygisk_vector/cli modules enable <pkg>` and `cli scope set <pkg> <app>/0`.
**Take the app off the Magisk DenyList**: the DenyList strips every Zygisk module from the process,
including the hook.
### 12.3 Keystore: transplanting app keys
Hardware-backed keys never leave the TEE, but keystore2 stores them **wrapped** in its SQLite DB.
If the new ROM runs on the same TEE (compare `keyentry.km_uuid` old vs new — same device, same
firmware ⇒ same), the old wrapped blobs still unwrap. Keys whose `blobmetadata` has tag 0
(`EncryptedBy`) are additionally super-encrypted with the **old lock-screen credential's** key and
cannot be moved. Domain 0 (APP) rows have `namespace = uid` → remap. Stop the apps, back up the live
DB, insert `keyentry`/`blobentry`/`blobmetadata`/`keyparameter`/`keymetadata` with fresh ids, reboot.
Apps that already regenerated keys on the new ROM also need their data re-restored to match.
`scripts/android/keystore-transplant.py OLD_DB [--replace] OLD_UID:NEW_UID …` writes the SQL; dry-run
it on a host copy of the live DB (`pragma integrity_check`) before applying with the device's own
`sqlite3`. Tested: transplanted keys load with no keystore errors after a reboot. That does not
bring back sessions the server ended, or apps that refuse to run because their build is too old.
### 12.4 Verify like a user would
- `test-all-apps.sh OUT` cold-starts every user app (unfreezing and re-freezing), screenshots it,
and greps its log for crashes, ANRs and keystore/crypto errors → `results.tsv`. Look at every
screenshot: logouts, licence checks, ToS/"what's new" dialogs and permission prompts don't crash.
- Android 17 shell launches must match the activity's intent filter:
`am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n 'pkg/act'`
(single-quote it — component names contain `$`). `-n` alone gives "Access blocked".
- `measure.sh OUT [pkg…]` snapshots memory, crashes/ANRs/lmk kills since boot and cold-launch
times; run it on the old ROM before the wipe so there is a baseline.
- Keep a running log of every security setting weakened during the restore (adb stay-awake, root
grants, doze exemptions, …) with its revert command; the restore needs many of them and nobody
remembers them afterwards.