diff --git a/README.md b/README.md index 2b360c8..e273015 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ avbroot applies two patches to the boot images: * Any operation that causes an unsigned or differently-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes: - * Performing a regular (unpatched) A/B OTA update. Disabling the `Automatic system updates` option in Android's Developer Options is recommended. + * Performing a regular (unpatched) A/B OTA update. * The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well. @@ -125,6 +125,18 @@ To update Android or Magisk: 4. Reboot. +### Blocking A/B OTA Updates + +Unpatched OTA updates are already blocked in recovery due to the replacing of the OTA signature verification certificates. To disable OTAs while booted into Android, turn off `Automatic system updates` in Android's Developer Options. + +To intentionally make A/B OTAs fail while booted into Android (to prevent accidental manual updates), build the `clearotacerts` module: + +```bash +python clearotacerts/build.py +``` + +and flash the `clearotacerts/dist/clearotacerts-.zip` file in Magisk. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail. + ### Implementation Details * avbroot relies on AOSP's avbtool, OTA utilities, and signapk. These are collections of applications that aren't meant to be used as libraries, but avbroot shoehorns them in anyway. Aside from signapk, these tools are not called via CLI because avbroot requires more control over the operations being performed than what is provided via the CLI interfaces. This "integration" is incredibly hacky and will likely require changes whenever the submodules are updated to point to newer AOSP commits. diff --git a/clearotacerts/META-INF/com/google/android/update-binary b/clearotacerts/META-INF/com/google/android/update-binary new file mode 100644 index 0000000..28b48e5 --- /dev/null +++ b/clearotacerts/META-INF/com/google/android/update-binary @@ -0,0 +1,33 @@ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 diff --git a/clearotacerts/META-INF/com/google/android/updater-script b/clearotacerts/META-INF/com/google/android/updater-script new file mode 100644 index 0000000..11d5c96 --- /dev/null +++ b/clearotacerts/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/clearotacerts/build.py b/clearotacerts/build.py new file mode 100644 index 0000000..8e6f1b8 --- /dev/null +++ b/clearotacerts/build.py @@ -0,0 +1,62 @@ +import io +import os +import shutil +import sys +import zipfile + + +def build_empty_zip(): + stream = io.BytesIO() + + with zipfile.ZipFile(stream, 'w') as z: + pass + + return stream.getvalue() + + +def parse_props(raw_prop): + result = {} + + for line in raw_prop.decode('UTF-8').splitlines(): + k, delim, v = line.partition('=') + if not delim: + raise ArgumentError(f'Malformed line: {repr(line)}') + + result[k.strip()] = v.strip() + + return result + + +def main(): + dist_dir = os.path.join(sys.path[0], 'dist') + os.makedirs(dist_dir, exist_ok=True) + + module_prop_path = os.path.join(sys.path[0], 'module.prop') + + with open(os.path.join(sys.path[0], 'module.prop'), 'rb') as f: + module_prop_raw = f.read() + module_prop = parse_props(module_prop_raw) + + name = module_prop['name'] + version = module_prop['version'].removeprefix('v') + zip_path = os.path.join(dist_dir, f'{name}-{version}.zip') + + with zipfile.ZipFile(zip_path, 'w') as z: + for name, data in ( + ('META-INF/com/google/android/update-binary', None), + ('META-INF/com/google/android/updater-script', None), + ('module.prop', module_prop_raw), + ('system/etc/security/otacerts.zip', build_empty_zip()), + ): + # Build our own ZipInfo to ensure archive is reproducible + info = zipfile.ZipInfo(name) + with z.open(info, 'w') as f_out: + if data is not None: + f_out.write(data) + else: + with open(os.path.join(sys.path[0], name), 'rb') as f_in: + shutil.copyfileobj(f_in, f_out) + + +if __name__ == '__main__': + main() diff --git a/clearotacerts/module.prop b/clearotacerts/module.prop new file mode 100644 index 0000000..ffeb770 --- /dev/null +++ b/clearotacerts/module.prop @@ -0,0 +1,6 @@ +id=com.chiller3.avbroot.clearotacerts +name=clearotacerts +version=v1.0 +versionCode=1 +author=chenxiaolong +description=Block A/B OTAs by clearing verification certificates