mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bad6f8c6f | |||
| 4ce4863237 | |||
| f615bf48cc | |||
| b505b14574 | |||
| e2adff6eed | |||
| 10370692d0 | |||
| 73b6e9b177 | |||
| 8d25e28ea3 | |||
| 82fc1336f7 | |||
| 953c0c1c5b | |||
| c3b073cece | |||
| 565efc5ee5 | |||
| 6ef8e548a3 | |||
| 80549346ea | |||
| 275b18b2a5 | |||
| 0a68966594 | |||
| 9ef5629556 | |||
| 1db09eb896 | |||
| a134d7f889 | |||
| 833ca931e8 | |||
| 9b6677d28a | |||
| 12e59fcf53 | |||
| 4bc7363f70 | |||
| b5ad9dddf6 | |||
| f0aba89549 | |||
| c9633682ef | |||
| da17935972 | |||
| c2140a85e9 | |||
| 3a3582ce4c | |||
| b029116a5a | |||
| af1b01646d | |||
| e85102709f | |||
| 124629dc45 | |||
| e89c4bdc9e | |||
| c1095b8321 | |||
| 17504a7f81 | |||
| d8190143bf | |||
| 0944abcea2 | |||
| 249c36d8c3 | |||
| 446e87289f | |||
| 04b8e3c315 | |||
| 03809a1e88 | |||
| 1fcb29532d | |||
| b383c8a0ab | |||
| 7ea8e8fa9e | |||
| ce43b3c657 | |||
| 05e5e59d65 | |||
| 72b8120bf9 | |||
| 8110f82b17 | |||
| 64dbfa241e | |||
| 7041c8ef2c | |||
| 7296eed51f | |||
| 698810d256 | |||
| acf57272a7 | |||
| 2b50e4527f | |||
| cd7b5ab23c | |||
| f73c451d23 | |||
| a801e18595 | |||
| 3932ff9092 | |||
| 1a36fbd46c | |||
| 077a80f4ce | |||
| 1e43930f3c | |||
| d00e53fb2a | |||
| 0b5f3b30cf | |||
| 10c2a0969f | |||
| 83218a747d | |||
| 6fb7d568cd | |||
| 6cbf690e17 | |||
| e097f98404 | |||
| d5605eabc0 | |||
| d1bcf2da80 | |||
| bbf8e28a28 | |||
| dc93b17888 | |||
| f83a408dc4 | |||
| 798fddc4d5 | |||
| dfed06dcf8 | |||
| 02fd92a640 | |||
| dd4faf72ae | |||
| adbe249edb | |||
| b7028b13a2 | |||
| 8122b0eece | |||
| 0613de4ee4 | |||
| 1a18eb7f85 | |||
| 1e1ca9dcbf | |||
| 6924783f48 | |||
| 98071daa33 | |||
| 8e1adae947 | |||
| d6705f4f00 | |||
| a648df1695 | |||
| a7c872be3e | |||
| cf1ab6ecca | |||
| 7364e8d725 | |||
| 935a86e72c | |||
| a9a6107043 | |||
| 3c1d5a8bb3 | |||
| 26ec8098e5 |
@@ -1,48 +0,0 @@
|
||||
name: Preload img cache
|
||||
inputs:
|
||||
cache-key-prefix:
|
||||
description: 'Device cache-key prefix'
|
||||
required: true
|
||||
device:
|
||||
description: 'Device name'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/cache@v3
|
||||
id: cache-img
|
||||
with:
|
||||
key: ${{ inputs.cache-key-prefix }}${{ inputs.device }}
|
||||
# Make sure any changes to path are also reflected in ci.yml setup
|
||||
path: e2e/files/${{ inputs.device }}-sparse.tar
|
||||
|
||||
- if: ${{ steps.cache-img.outputs.cache-hit }}
|
||||
name: Extracting image from sparse archive
|
||||
shell: sh
|
||||
working-directory: e2e/files
|
||||
run: tar -xf ${{ inputs.device }}-sparse.tar
|
||||
|
||||
- name: Restore e2e executable
|
||||
if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
- name: Downloading device image for ${{ inputs.device }}
|
||||
if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
shell: sh
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e download --stripped -d ${{ inputs.device }}
|
||||
|
||||
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
name: Creating sparse archive from image
|
||||
shell: sh
|
||||
working-directory: e2e/files
|
||||
run: |
|
||||
tar --sparse -cf ${{ inputs.device }}-sparse.tar \
|
||||
${{ inputs.device }}/*.stripped
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Preload Magisk
|
||||
inputs:
|
||||
cache-key:
|
||||
description: 'Magisk cache-key'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/cache@v3
|
||||
id: cache-magisk
|
||||
with:
|
||||
key: ${{ inputs.cache-key }}
|
||||
# Make sure any changes to path are also reflected in ci.yml setup
|
||||
path: e2e/files/magisk
|
||||
|
||||
- name: Restore e2e executable
|
||||
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
- name: Downloading Magisk
|
||||
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
shell: sh
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e download --magisk
|
||||
+34
-147
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -C strip=symbols
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -40,28 +40,42 @@ jobs:
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Get Rust LLVM target triple
|
||||
- name: Get Rust target triple
|
||||
id: get_target
|
||||
shell: bash
|
||||
env:
|
||||
RUSTC_BOOTSTRAP: '1'
|
||||
run: |
|
||||
echo -n 'name=' >> "${GITHUB_OUTPUT}"
|
||||
rustc -Z unstable-options --print target-spec-json \
|
||||
| jq -r '."llvm-target"' \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
rustc -vV | sed -n 's|host: ||p' >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
cargo clippy --release --workspace --features static \
|
||||
--target ${{ steps.get_target.outputs.name }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
cargo build --release --workspace --features static \
|
||||
--target ${{ steps.get_target.outputs.name }}
|
||||
|
||||
- name: Tests
|
||||
run: cargo test --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
cargo test --release --workspace --features static \
|
||||
--target ${{ steps.get_target.outputs.name }}
|
||||
|
||||
- name: End to end tests
|
||||
shell: bash
|
||||
run: |
|
||||
cargo run --release -p e2e --features static \
|
||||
--target ${{ steps.get_target.outputs.name }} \
|
||||
-- test -a -c e2e/e2e.toml
|
||||
|
||||
- name: Archive documentation
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -71,148 +85,21 @@ jobs:
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
# Due to https://github.com/rust-lang/rust/issues/78210, we have to use
|
||||
# the --target option, which puts all output files in a different path.
|
||||
# Symlink that path to the normal output directory so that we don't need
|
||||
# to specify the Rust triple everywhere.
|
||||
- name: Symlink target directory
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf target/output
|
||||
ln -s ${{ steps.get_target.outputs.name }}/release target/output
|
||||
|
||||
# This is separate so we can have a flat directory structure.
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
|
||||
path: |
|
||||
target/release/avbroot
|
||||
target/release/avbroot.exe
|
||||
|
||||
- name: Cache e2e executable
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
setup:
|
||||
name: Prepare workflow data
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
config-path: ${{ steps.load-config.outputs.config-path }}
|
||||
device-list: ${{ steps.load-config.outputs.device-list }}
|
||||
magisk-key: ${{ steps.cache-keys.outputs.magisk-key }}
|
||||
img-key-prefix: ${{ steps.cache-keys.outputs.img-key-prefix }}
|
||||
img-hit: ${{ steps.get-img-cache.outputs.cache-matched-key }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Restore e2e executable
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
- name: Loading test config
|
||||
id: load-config
|
||||
working-directory: e2e
|
||||
run: |
|
||||
echo 'config-path=e2e/e2e.toml' >> "${GITHUB_OUTPUT}"
|
||||
echo -n 'device-list=' >> "${GITHUB_OUTPUT}"
|
||||
../target/release/e2e list \
|
||||
| jq -cnR '[inputs | select(length > 0)]' \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Generating cache keys
|
||||
id: cache-keys
|
||||
run: |
|
||||
{
|
||||
echo "img-key-prefix=img-${{ hashFiles(steps.load-config.outputs.config-path) }}-"; \
|
||||
echo "magisk-key=magisk-${{ hashFiles(steps.load-config.outputs.config-path) }}";
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checking for cached device images
|
||||
id: get-img-cache
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: ${{ steps.cache-keys.outputs.img-key-prefix }}
|
||||
lookup-only: true
|
||||
path: |
|
||||
e2e/files/${{ fromJSON(steps.load-config.outputs.device-list)[0] }}-sparse.tar
|
||||
|
||||
- name: Checking for cached magisk apk
|
||||
id: get-magisk-cache
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: ${{ steps.cache-keys.outputs.magisk-key }}
|
||||
lookup-only: true
|
||||
path: e2e/files/magisk
|
||||
|
||||
- name: Preloading Magisk cache
|
||||
if: ${{ ! steps.get-magisk-cache.outputs.cache-hit }}
|
||||
uses: ./.github/actions/preload-magisk-cache
|
||||
with:
|
||||
cache-key: ${{ steps.cache-keys.outputs.magisk-key }}
|
||||
|
||||
preload-img:
|
||||
name: Preload device images
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
timeout-minutes: 5
|
||||
# Assume that preloading always succesfully cached all images before.
|
||||
# If for some reason only some got cached, on the first run, the cache will not be preloaded
|
||||
# which will result in some being downloaded multiple times when running the tests.
|
||||
if: ${{ ! needs.setup.outputs.img-hit }}
|
||||
strategy:
|
||||
matrix:
|
||||
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Preloading image cache
|
||||
uses: ./.github/actions/preload-img-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
|
||||
device: ${{ matrix.device }}
|
||||
|
||||
tests:
|
||||
name: Run test for ${{ matrix.device }} on ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- setup
|
||||
- preload-img
|
||||
timeout-minutes: 10
|
||||
# Continue on skipped but not on failures or cancels
|
||||
if: ${{ always() && ! failure() && ! cancelled() }}
|
||||
strategy:
|
||||
matrix:
|
||||
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Restoring Magisk cache
|
||||
uses: ./.github/actions/preload-magisk-cache
|
||||
with:
|
||||
cache-key: ${{ needs.setup.outputs.magisk-key }}
|
||||
|
||||
- name: Restoring image cache
|
||||
uses: ./.github/actions/preload-img-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
|
||||
device: ${{ matrix.device }}
|
||||
|
||||
- name: Restore e2e executable
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
# Finally run tests
|
||||
- name: Run test for ${{ matrix.device }}
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e test --stripped -d ${{ matrix.device }}
|
||||
target/output/avbroot
|
||||
target/output/avbroot.exe
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Modules
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
name: Build modules
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
echo -n 'version=' >> "${GITHUB_OUTPUT}"
|
||||
git describe --always \
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build modules
|
||||
run: cargo xtask modules -a
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: avbroot-modules-${{ steps.get_version.outputs.version }}
|
||||
path: modules/dist/
|
||||
@@ -7,6 +7,62 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.1.1
|
||||
|
||||
* Cache salted SHA-256 contexts for a small performance improvement ([PR #257])
|
||||
* Fix loading certificates that have extra text outside of the marker lines ([PR #261])
|
||||
|
||||
### Version 3.1.0
|
||||
|
||||
* The `OEMUnlockOnBoot` module has been split out to a separate repo ([Discussion #235], [PR #246])
|
||||
* https://github.com/chenxiaolong/OEMUnlockOnBoot
|
||||
* The new module supports the automatic update mechanism within Magisk/KernelSU
|
||||
* Add support for Magisk v27.0 ([PR #255])
|
||||
* Switch to using a proper logging library ([PR #251])
|
||||
* Folks who want to see the juicy details during patching can use `--log-level debug` or `--log-level trace`
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Switch from xz2 to liblzma (maintained fork of xz2) ([PR #247])
|
||||
* Update all dependencies ([PR #256])
|
||||
|
||||
### Version 3.0.0
|
||||
|
||||
Happy New Year! This release brings two major changes:
|
||||
|
||||
1. The OTA certificates (`otacerts.zip`) in the system partition are now patched. The `clearotacerts` module from avbroot (or the `customotacerts` module from Custota) are no longer needed and can be safely uninstalled.
|
||||
|
||||
This makes it possible to use Pixel's new Repair Mode safely. To do so, follow the instructions in the [documentation here](./README.md#repair-mode).
|
||||
|
||||
2. Autodetection for boot partitions is now significantly more reliable. For KernelSU users or folks who have more obscure devices, the `--boot-partition` option is no longer required (and is now ignored).
|
||||
|
||||
Full list of changes:
|
||||
|
||||
* Add support for AVB 2.0 format 1.3.0 (for Android 15) ([PR #210])
|
||||
* Add new `avbroot key decode-avb` command for converting AVB-encoded public keys to the standard PKCS8-encoded format ([PR #219])
|
||||
* Improve autodetection of boot images ([Issue #218], [PR #221], [PR #237])
|
||||
* Build precompiled executables as statically linked executables ([Issue #222], [PR #224], [PR #227])
|
||||
* Limit critical partition check to bootloader-verified partitions ([Issue #223], [PR #226])
|
||||
* Improve patching performance by spliiting new partition images into chunks and compressing them in parallel ([PR #228])
|
||||
* Also verify whole-partition hashes when running `avbroot ota verify` ([PR #229])
|
||||
* Add support for patching `otacerts.zip` on the system partition ([Issue #225], [PR #240], [PR #244])
|
||||
* Document how to use Repair Mode safely ([Issue #216], [PR #243])
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Fix lint warnings introduced in Rust 1.74.0 ([PR #211])
|
||||
* Temporarily silence [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071) warning in cargo-deny ([PR #214])
|
||||
* Add support for partially updating FEC data ([PR #230], [PR #231], [PR #234])
|
||||
* Fix hash tree calculation for images smaller than one block ([PR #232])
|
||||
* Refactor hash tree code and add tests, CLI commands, and support for partial updates ([PR #233])
|
||||
* Generate mock OTAs to use for end-to-end tests ([PR #241])
|
||||
* Update all dependencies ([PR #245])
|
||||
|
||||
### Version 2.3.3
|
||||
|
||||
* Add support for XZ-compressed ramdisks ([Issue #203], [PR #207])
|
||||
* Merge property and kernel command line AVB descriptors when replacing partitions ([Issue #203], [PR #208])
|
||||
|
||||
### Version 2.3.2
|
||||
|
||||
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
|
||||
@@ -103,6 +159,7 @@ Behind-the-scenes changes:
|
||||
|
||||
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
|
||||
[Discussion #195]: https://github.com/chenxiaolong/avbroot/discussions/195
|
||||
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
|
||||
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
|
||||
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
|
||||
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
|
||||
@@ -112,6 +169,11 @@ Behind-the-scenes changes:
|
||||
[Issue #166]: https://github.com/chenxiaolong/avbroot/issues/166
|
||||
[Issue #201]: https://github.com/chenxiaolong/avbroot/issues/201
|
||||
[Issue #203]: https://github.com/chenxiaolong/avbroot/issues/203
|
||||
[Issue #216]: https://github.com/chenxiaolong/avbroot/issues/216
|
||||
[Issue #218]: https://github.com/chenxiaolong/avbroot/issues/218
|
||||
[Issue #222]: https://github.com/chenxiaolong/avbroot/issues/222
|
||||
[Issue #223]: https://github.com/chenxiaolong/avbroot/issues/223
|
||||
[Issue #225]: https://github.com/chenxiaolong/avbroot/issues/225
|
||||
[PR #130]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
[PR #132]: https://github.com/chenxiaolong/avbroot/pull/132
|
||||
[PR #133]: https://github.com/chenxiaolong/avbroot/pull/133
|
||||
@@ -159,3 +221,33 @@ Behind-the-scenes changes:
|
||||
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
|
||||
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
|
||||
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
|
||||
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
|
||||
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
|
||||
[PR #210]: https://github.com/chenxiaolong/avbroot/pull/210
|
||||
[PR #211]: https://github.com/chenxiaolong/avbroot/pull/211
|
||||
[PR #214]: https://github.com/chenxiaolong/avbroot/pull/214
|
||||
[PR #219]: https://github.com/chenxiaolong/avbroot/pull/219
|
||||
[PR #221]: https://github.com/chenxiaolong/avbroot/pull/221
|
||||
[PR #224]: https://github.com/chenxiaolong/avbroot/pull/224
|
||||
[PR #226]: https://github.com/chenxiaolong/avbroot/pull/226
|
||||
[PR #227]: https://github.com/chenxiaolong/avbroot/pull/227
|
||||
[PR #228]: https://github.com/chenxiaolong/avbroot/pull/228
|
||||
[PR #229]: https://github.com/chenxiaolong/avbroot/pull/229
|
||||
[PR #230]: https://github.com/chenxiaolong/avbroot/pull/230
|
||||
[PR #231]: https://github.com/chenxiaolong/avbroot/pull/231
|
||||
[PR #232]: https://github.com/chenxiaolong/avbroot/pull/232
|
||||
[PR #233]: https://github.com/chenxiaolong/avbroot/pull/233
|
||||
[PR #234]: https://github.com/chenxiaolong/avbroot/pull/234
|
||||
[PR #237]: https://github.com/chenxiaolong/avbroot/pull/237
|
||||
[PR #240]: https://github.com/chenxiaolong/avbroot/pull/240
|
||||
[PR #241]: https://github.com/chenxiaolong/avbroot/pull/241
|
||||
[PR #243]: https://github.com/chenxiaolong/avbroot/pull/243
|
||||
[PR #244]: https://github.com/chenxiaolong/avbroot/pull/244
|
||||
[PR #245]: https://github.com/chenxiaolong/avbroot/pull/245
|
||||
[PR #246]: https://github.com/chenxiaolong/avbroot/pull/246
|
||||
[PR #247]: https://github.com/chenxiaolong/avbroot/pull/247
|
||||
[PR #251]: https://github.com/chenxiaolong/avbroot/pull/251
|
||||
[PR #255]: https://github.com/chenxiaolong/avbroot/pull/255
|
||||
[PR #256]: https://github.com/chenxiaolong/avbroot/pull/256
|
||||
[PR #257]: https://github.com/chenxiaolong/avbroot/pull/257
|
||||
[PR #261]: https://github.com/chenxiaolong/avbroot/pull/261
|
||||
|
||||
Generated
+417
-474
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "2.3.2"
|
||||
version = "3.1.1"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -175,6 +175,14 @@ The default behavior is to use 2 bytes of parity information per 253 bytes of in
|
||||
|
||||
The number of parity bytes (between 2 and 24, inclusive) can be configured using `--parity`.
|
||||
|
||||
### Updating FEC data
|
||||
|
||||
```bash
|
||||
avbroot fec update -i <input data file> -f <FEC file> [-r <start> <end>]...
|
||||
```
|
||||
|
||||
This will update the FEC data corresponding to the specified regions. This can be significantly faster than generating new FEC data from scratch for large files if the regions where data was modified are known.
|
||||
|
||||
### Verifying a file
|
||||
|
||||
```bash
|
||||
@@ -194,3 +202,49 @@ avbroot fec repair -i <input/output data file> -f <input FEC file>
|
||||
This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected.
|
||||
|
||||
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data.
|
||||
|
||||
## `avbroot hash-tree`
|
||||
|
||||
This set of commands is for working with dm-verity hash tree data. They are not especially useful outside of debugging avbroot itself because the output format is custom. There is a custom header that sits in front of the standard dm-verity hash tree data.
|
||||
|
||||
| Offsets | Type | Description |
|
||||
|------------|--------|--------------------------------|
|
||||
| 0..16 | ASCII | `avbroot!hashtree` magic bytes |
|
||||
| 16..18 | U16LE | Version (currently 1) |
|
||||
| 18..26 | U64LE | Image size |
|
||||
| 26..30 | U32LE | Block size |
|
||||
| 30..46 | ASCII | Hash algorithm |
|
||||
| 46..48 | U16LE | Salt size |
|
||||
| 48..50 | U16LE | Root digest size |
|
||||
| 50..54 | U32LE | Hash tree size |
|
||||
| (Variable) | BINARY | Salt |
|
||||
| (Variable) | BINARY | Root digest |
|
||||
| (Variable) | BINARY | Hash tree |
|
||||
|
||||
For more information on the hash tree data, see the [Linux kernel documentation](https://docs.kernel.org/admin-guide/device-mapper/verity.html#hash-tree) or avbroot's implementation in [`hashtree.rs`](./avbroot/src/format/hashtree.rs).
|
||||
|
||||
### Generating hash tree
|
||||
|
||||
```bash
|
||||
avbroot hash-tree generate -i <input data file> -H <output hash tree file>
|
||||
```
|
||||
|
||||
The default behavior is to use a block size of 4096, the `sha256` algorithm, and an empty salt. These can be changed with the `-b`, `-a`, and `-s` options, respectively.
|
||||
|
||||
All parameters needed for verification are included in the hash tree file's header.
|
||||
|
||||
### Updating hash tree
|
||||
|
||||
```bash
|
||||
avbroot hash-tree update -i <input data file> -H <hash tree file> [-r <start> <end>]...
|
||||
```
|
||||
|
||||
This will update the hash tree data corresponding to the specified regions. This can be significantly faster than generating new hash tree data from scratch for large files if the regions where data was modified are known.
|
||||
|
||||
### Verifying a file
|
||||
|
||||
```bash
|
||||
avbroot hash-tree verify -i <input data file> -H <input hash tree file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
|
||||
|
||||
@@ -13,27 +13,27 @@ Having a good understanding of how AVB and A/B OTAs work is recommended prior to
|
||||
* `payload.bin` exists
|
||||
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
|
||||
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders support this, as well as most OnePlus devices. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders since the Pixel 2 support this. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
|
||||
* NOTE: Some OnePlus devices have a broken implementation where a custom public key can be set, but the device won't boot despite having proper signatures. Downgrading the bootloader to the version shipped with Android 11 might potentially help. This problem has been reported across multiple OnePlus models ([#186](https://github.com/chenxiaolong/avbroot/issues/186), [#195](https://github.com/chenxiaolong/avbroot/discussions/195), [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
|
||||
## Patches
|
||||
|
||||
avbroot applies two patches to the boot images:
|
||||
avbroot applies the following patches to the partition images:
|
||||
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
|
||||
|
||||
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded from recovery mode after the bootloader has been locked. It also prevents accidental flashing of the original unpatched OTA.
|
||||
|
||||
* The `system` or `my_engineering` image, depending on device, is also patched to replace the OTA signature verification certificates. This prevents the OS' system updater app from installing an unpatched OTA and also allows the use of custom OTA updater apps.
|
||||
|
||||
## Warnings and Caveats
|
||||
|
||||
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
|
||||
|
||||
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
|
||||
|
||||
* Any operation that causes an improperly-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 unpatched A/B OTA update while booted into Android via the OS' default updater. This can be blocked via a Magisk/KernelSU module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
|
||||
|
||||
* The `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
|
||||
* Any operation that causes an improperly-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 the `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
|
||||
|
||||
If the boot image is ever modified, **do not reboot**. [Open an issue](https://github.com/chenxiaolong/avbroot/issues/new) for support and be very clear about what steps were done that lead to the situation. If Android is still running and root access works, it might be possible to recover without wiping and starting over.
|
||||
|
||||
@@ -77,8 +77,7 @@ avbroot applies two patches to the boot images:
|
||||
* To enable root access with KernelSU:
|
||||
|
||||
```bash
|
||||
--prepatched /path/to/kernelsu/boot.img \
|
||||
--boot-partition @gki_kernel
|
||||
--prepatched /path/to/kernelsu/boot.img
|
||||
```
|
||||
|
||||
* To leave the OS unrooted:
|
||||
@@ -143,19 +142,19 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
If you are using KernelSU, also add `--boot-partition @gki_kernel` to the command.
|
||||
|
||||
4. Flash the partition images that were extracted.
|
||||
|
||||
This can be done by manually running `fastboot flash <partition> extracted/<partition>.img` for each image in the `extracted/` directory or by using the following script:
|
||||
For each partition inside `extracted/`, except for `system`, run:
|
||||
|
||||
```bash
|
||||
for image in extracted/*.img; do
|
||||
partition=$(basename "${image}")
|
||||
partition=${partition%.img}
|
||||
fastboot flash <partition> extracted/<partition>.img
|
||||
```
|
||||
|
||||
fastboot flash "${partition}" "${image}"
|
||||
done
|
||||
Then, reboot into recovery's fastbootd mode and flash `system`:
|
||||
|
||||
```bash
|
||||
fastboot reboot fastboot
|
||||
fastboot flash system extracted/system.img
|
||||
```
|
||||
|
||||
5. Set up the custom AVB public key in the bootloader.
|
||||
@@ -183,12 +182,10 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
|
||||
Remember: **Do not uncheck `OEM unlocking`!**
|
||||
|
||||
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using [avbroot's `oemunlockonboot` Magisk module](#oemunlockonboot-enable-oem-unlocking-on-every-boot) to automatically ensure OEM unlocking is enabled on every boot.
|
||||
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using the [`OEMUnlockOnBoot` module](https://github.com/chenxiaolong/OEMUnlockOnBoot) to automatically ensure OEM unlocking is enabled on every boot.
|
||||
|
||||
8. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
|
||||
|
||||
For extra safety, consider flashing [avbroot's Magisk/KernelSU modules](#avbroot-modules).
|
||||
|
||||
## Updates
|
||||
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
|
||||
@@ -219,28 +216,36 @@ To stop using avbroot and revert to the stock firmware:
|
||||
|
||||
4. That's it! There are no other remnants to clean up.
|
||||
|
||||
## avbroot modules
|
||||
## OTA updates
|
||||
|
||||
avbroot's Magisk/KernelSU modules can be downloaded from the [releases page](https://github.com/chenxiaolong/avbroot/releases).
|
||||
avbroot replaces `/system/etc/security/otacerts.zip` in both the system and recovery partitions with a new zip that contains the custom OTA signing certificate. This prevents an unpatched OTA from inadvertently being installed both when booted into Android and when sideloading from recovery.
|
||||
|
||||
### `clearotacerts`: Block OTA Updates from default updater app
|
||||
|
||||
Unpatched OTA updates are already blocked when booted into recovery mode because the original OTA certificate has been replaced with the custom certificate. However, this doesn't prevent the Android's system updater app from attempting to install an unpatched OTA update.
|
||||
|
||||
Disabling the system updater app is recommended. To do so:
|
||||
Disabling the system updater app is recommended to prevent it from even attempting to install an unpatched OTA. To do so:
|
||||
|
||||
* Stock OS: Turn off `Automatic system updates` in Android's Developer Options.
|
||||
* Custom OS: Disable the system updater app (or block its network access) from Settings -> Apps -> See all apps -> (three-dot menu) -> Show system -> (find updater app).
|
||||
|
||||
As an extra safety measure, flashing the `clearotacerts` module will intentionally make OTAs fail to install while booted into Android. It does so by overriding `/system/etc/security/otacerts.zip` with an empty zip containing no certificates so that even if an OTA is downloaded, signature verification will fail. This may cause some custom OS' system updater app to get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails, so make sure the system updater app is disabled.
|
||||
This is especially important for some custom OS's because their system updater app may get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails.
|
||||
|
||||
As an alternative to this module, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that installs updates from a self-hosted OTA server.
|
||||
To self-host a custom OTA server, see [Custota](https://github.com/chenxiaolong/Custota).
|
||||
|
||||
### `oemunlockonboot`: Enable OEM unlocking on every boot
|
||||
## Repair mode
|
||||
|
||||
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS' initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
|
||||
Some devices now ship with a Repair Mode feature that boots the system with a fresh `userdata` image so that repair technicians are able to run on-device diagnostics without needing the user's credentials to unlock the device.
|
||||
|
||||
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
|
||||
When the device is rooted, it is unsafe to use Repair Mode. Unless you are using release builds of Magisk/KernelSU signed with your own keys, it's trivial for someone to just install the Magisk/KernelSU app while in repair mode to gain root access with no authentication.
|
||||
|
||||
To safely use Repair Mode:
|
||||
|
||||
1. Unroot the device by repatching the OTA with the `--rootless` option (instead of `--magisk` or `--prepatched`) and flashing it.
|
||||
|
||||
2. Turn on Repair Mode.
|
||||
|
||||
3. After receiving the repaired device, exit Repair Mode.
|
||||
|
||||
4. Flash the (rooted) patched OTA as normal.
|
||||
|
||||
Because the unrooting and rooting are done by flashing OTAs, the device's data will not be wiped.
|
||||
|
||||
## Magisk preinit device
|
||||
|
||||
@@ -331,9 +336,7 @@ Invoke-Expression (& avbroot completion -s powershell)
|
||||
|
||||
### Using a prepatched boot image
|
||||
|
||||
avbroot can replace the boot image with a prepatched image instead of applying the root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched Magisk boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
|
||||
|
||||
For KernelSU, also pass in `--boot-partition @gki_kernel` for both the `patch` and `extract` commands. avbroot defaults to Magisk's semantics where the boot image containing the GKI ramdisk is needed, whereas KernelSU requires the boot image containing the GKI kernel. This only affects devices launching with Android 13+, where the GKI kernel and ramdisk are in different partitions (`boot` vs. `init_boot`), but it is safe and recommended to always use this option for KernelSU.
|
||||
avbroot can replace the boot image with a prepatched image instead of applying the root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched Magisk boot image or a KernelSU boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
|
||||
|
||||
Note that avbroot will validate that the prepatched image is compatible with the original. If, for example, the header fields do not match or a boot image section is missing, then the patching process will abort. The checks are not foolproof, but should help protect against accidental use of the wrong boot image. To bypass a somewhat "safe" subset of the checks, use `--ignore-prepatched-compat`. To ignore all checks (strongly discouraged!), pass it in twice.
|
||||
|
||||
@@ -420,14 +423,6 @@ Debug builds work too, but they will run significantly slower (in the sha256 com
|
||||
|
||||
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
|
||||
|
||||
To build avbroot's modules from source, run:
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
```
|
||||
|
||||
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable must be set to the Android SDK path.
|
||||
|
||||
## Verifying digital signatures
|
||||
|
||||
First, save the public key to a file listing the keys to be trusted. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
|
||||
|
||||
+8
-12
@@ -11,10 +11,11 @@ publish = false
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
base64 = "0.21.3"
|
||||
bitflags = "2.4.1"
|
||||
bstr = "1.6.2"
|
||||
byteorder = "1.4.3"
|
||||
cap-std = "2.0.0"
|
||||
cap-tempfile = "2.0.0"
|
||||
cap-std = "3.0.0"
|
||||
cap-tempfile = "3.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
clap_complete = "4.4.0"
|
||||
cms = { version = "0.2.2", features = ["std"] }
|
||||
@@ -23,6 +24,7 @@ ctrlc = "3.4.0"
|
||||
flate2 = "1.0.27"
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.2.1"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
@@ -45,8 +47,10 @@ sha1 = "0.10.5"
|
||||
sha2 = "0.10.7"
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "1.0.47"
|
||||
toml_edit = { version = "0.20.1", features = ["serde"] }
|
||||
toml_edit = { version = "0.21.0", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
|
||||
# There's an upstream bug that causes an infinite loop in the write::BzDecoder
|
||||
@@ -58,14 +62,6 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
git = "https://github.com/jongiddy/bzip2-rs"
|
||||
rev = "2aefcb4d3634de1df226c73d93f758d65228bb8c"
|
||||
|
||||
# The upstream xz2 crate uses an old version of liblzma when compiling with the
|
||||
# `static` feature and doesn't enable all of the encoders and decoders. This
|
||||
# causes certain payload data to fail to decompress.
|
||||
# https://github.com/chenxiaolong/avbroot/issues/138
|
||||
[dependencies.xz2]
|
||||
git = "https://github.com/chenxiaolong/xz2-rs"
|
||||
rev = "fe2050b9c3395db15d8610f1dabb505440c1a556"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
@@ -84,4 +80,4 @@ protox = "0.5.0"
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["bzip2/static", "xz2/static"]
|
||||
static = ["bzip2/static", "liblzma/static"]
|
||||
|
||||
+121
-4
@@ -3,12 +3,19 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, IsTerminal},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, key, ota};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -18,6 +25,7 @@ pub enum Command {
|
||||
Completion(completion::CompletionCli),
|
||||
Cpio(cpio::CpioCli),
|
||||
Fec(fec::FecCli),
|
||||
HashTree(hashtree::HashTreeCli),
|
||||
Key(key::KeyCli),
|
||||
Ota(ota::OtaCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
@@ -28,22 +36,131 @@ pub enum Command {
|
||||
MagiskInfo(boot::MagiskInfoCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn as_level(self) -> Level {
|
||||
match self {
|
||||
Self::Trace => Level::TRACE,
|
||||
Self::Debug => Level::DEBUG,
|
||||
Self::Info => Level::INFO,
|
||||
Self::Warn => Level::WARN,
|
||||
Self::Error => Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LogLevel {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum LogFormat {
|
||||
Short,
|
||||
Medium,
|
||||
Long,
|
||||
}
|
||||
|
||||
impl Default for LogFormat {
|
||||
fn default() -> Self {
|
||||
Self::Short
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
|
||||
pub log_level: LogLevel,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
|
||||
pub fn main(cancel_signal: &AtomicBool) -> Result<()> {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ShortUptime {
|
||||
epoch: Instant,
|
||||
}
|
||||
|
||||
impl Default for ShortUptime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
epoch: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatTime for ShortUptime {
|
||||
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
|
||||
let e = self.epoch.elapsed();
|
||||
write!(w, "{:3}.{:03}s", e.as_secs(), e.subsec_millis())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
|
||||
let builder = tracing_subscriber::fmt()
|
||||
.with_writer(io::stderr)
|
||||
.with_ansi(io::stderr().is_terminal())
|
||||
.with_max_level(log_level.as_level());
|
||||
|
||||
match log_format {
|
||||
LogFormat::Short => {
|
||||
let format = tracing_subscriber::fmt::format()
|
||||
.with_timer(ShortUptime::default())
|
||||
.with_target(false);
|
||||
|
||||
builder.event_format(format).init();
|
||||
}
|
||||
LogFormat::Medium => {
|
||||
builder.with_timer(ShortUptime::default()).init();
|
||||
}
|
||||
LogFormat::Long => {
|
||||
builder.pretty().init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
init_logging(cli.log_level, cli.log_format);
|
||||
logging_initialized.store(true, Ordering::SeqCst);
|
||||
|
||||
debug!(?cli);
|
||||
|
||||
match cli.command {
|
||||
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
|
||||
Command::Boot(c) => boot::boot_main(&c),
|
||||
Command::Completion(c) => completion::completion_main(&c),
|
||||
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
|
||||
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
|
||||
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
// Deprecated aliases.
|
||||
|
||||
+27
-25
@@ -22,9 +22,9 @@ use clap::{Args, Parser, Subcommand};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use rsa::RsaPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug_span, info, warn, Span};
|
||||
|
||||
use crate::{
|
||||
cli::{status, warning},
|
||||
crypto::{self, PassphraseSource},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
@@ -99,7 +99,7 @@ fn promote_insecure_hash_algorithm(algorithm: &str) -> &str {
|
||||
const NEW_ALGORITHM: &str = "sha256";
|
||||
|
||||
if INSECURE_ALGORITHMS.contains(&algorithm) {
|
||||
warning!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
|
||||
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
|
||||
NEW_ALGORITHM
|
||||
} else {
|
||||
algorithm
|
||||
@@ -166,7 +166,7 @@ fn write_raw_and_verify(
|
||||
|
||||
if let Err(e) = result {
|
||||
if ignore_invalid {
|
||||
warning!("{e:?}");
|
||||
warn!("{e:?}");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
@@ -195,7 +195,7 @@ fn write_raw_and_update(
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
|
||||
d.image_size = image_size;
|
||||
d.update(&raw_file, &raw_file, cancel_signal)
|
||||
d.update(&raw_file, &raw_file, None, cancel_signal)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
}
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
@@ -358,16 +358,16 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
match sign_action {
|
||||
SignAction::None => {
|
||||
if originally_signed {
|
||||
status!("Preserving original AVB header signature");
|
||||
info!("Preserving original AVB header signature");
|
||||
} else {
|
||||
status!("Leaving AVB header unsigned");
|
||||
info!("Leaving AVB header unsigned");
|
||||
}
|
||||
}
|
||||
SignAction::Sign => {
|
||||
if originally_signed {
|
||||
status!("Replacing AVB header signature");
|
||||
info!("Replacing AVB header signature");
|
||||
} else {
|
||||
status!("Signing AVB header");
|
||||
info!("Signing AVB header");
|
||||
}
|
||||
|
||||
let Some(key_path) = &key_group.key else {
|
||||
@@ -389,9 +389,9 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
}
|
||||
SignAction::Clear => {
|
||||
if originally_signed {
|
||||
status!("Clearing AVB header signature");
|
||||
info!("Clearing AVB header signature");
|
||||
} else {
|
||||
status!("Leaving AVB header unsigned");
|
||||
info!("Leaving AVB header unsigned");
|
||||
}
|
||||
|
||||
info.header.algorithm_type = AlgorithmType::None;
|
||||
@@ -451,15 +451,15 @@ pub fn verify_headers(
|
||||
|
||||
if let Some(e) = expected_key {
|
||||
if k == e {
|
||||
status!("{prefix}");
|
||||
info!("{prefix}");
|
||||
} else {
|
||||
bail!("{prefix}, but is signed by an untrusted key");
|
||||
}
|
||||
} else {
|
||||
warning!("{prefix}, but parent does not list a trusted key");
|
||||
warn!("{prefix}, but parent does not list a trusted key");
|
||||
}
|
||||
} else {
|
||||
status!("{name} has an unsigned vbmeta header");
|
||||
info!("{name} has an unsigned vbmeta header");
|
||||
}
|
||||
|
||||
for descriptor in &header.descriptors {
|
||||
@@ -502,6 +502,7 @@ fn verify_and_repair(
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
|
||||
let suffix = match name {
|
||||
Some(n) => format!(" for: {n}"),
|
||||
None => String::new(),
|
||||
@@ -509,21 +510,18 @@ fn verify_and_repair(
|
||||
|
||||
match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
status!("Verifying hash tree descriptor{suffix}");
|
||||
info!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(&file, cancel_signal) {
|
||||
Err(
|
||||
e @ avb::Error::InvalidRootDigest { .. }
|
||||
| e @ avb::Error::InvalidHashTree { .. },
|
||||
) if repair => {
|
||||
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warning!("Attempting to repair using FEC data{suffix}");
|
||||
Err(e @ avb::Error::HashTree(_)) if repair => {
|
||||
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warn!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(&file, cancel_signal).map(|_| {
|
||||
status!("Successfully repaired data{suffix}");
|
||||
info!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
ret => ret,
|
||||
@@ -531,7 +529,7 @@ fn verify_and_repair(
|
||||
.with_context(|| format!("Failed to verify hash tree descriptor{suffix}"))?;
|
||||
}
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
status!("Verifying hash descriptor{suffix}");
|
||||
info!("Verifying hash descriptor{suffix}");
|
||||
|
||||
file.rewind()?;
|
||||
d.verify(file, cancel_signal)
|
||||
@@ -550,9 +548,13 @@ pub fn verify_descriptors(
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
descriptors
|
||||
.par_iter()
|
||||
.map(|(name, descriptor)| {
|
||||
let _span = parent_span.enter();
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let file = match directory
|
||||
.open_with(&path, OpenOptions::new().read(true).write(repair))
|
||||
@@ -563,7 +565,7 @@ pub fn verify_descriptors(
|
||||
// refer to partitions that exist on the device, but not in the
|
||||
// OTA.
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warning!("Partition image does not exist: {path:?}");
|
||||
warn!("Partition image does not exist: {path:?}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -645,7 +647,7 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
// There could have been errors in the original FEC data itself.
|
||||
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
|
||||
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
|
||||
d.update(&file, &file, cancel_signal)?;
|
||||
d.update(&file, &file, None, cancel_signal)?;
|
||||
}
|
||||
|
||||
update_dm_verity_cmdline(&mut info)?;
|
||||
@@ -709,7 +711,7 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
)?;
|
||||
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
|
||||
|
||||
status!("Successfully verified all vbmeta signatures and hashes");
|
||||
info!("Successfully verified all vbmeta signatures and hashes");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -61,6 +61,24 @@ fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let ranges = cli
|
||||
.range
|
||||
.chunks_exact(2)
|
||||
.map(|w| w[0]..w[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let mut fec = read_fec(&cli.fec)?;
|
||||
|
||||
fec.update(&input, &ranges, cancel_signal)
|
||||
.context("Failed to update FEC data")?;
|
||||
|
||||
write_fec(&cli.fec, &fec)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
@@ -87,6 +105,7 @@ fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
FecCommand::Generate(c) => generate_subcommand(c, cancel_signal),
|
||||
FecCommand::Update(c) => update_subcommand(c, cancel_signal),
|
||||
FecCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
FecCommand::Repair(c) => repair_subcommand(c, cancel_signal),
|
||||
}
|
||||
@@ -108,6 +127,26 @@ struct GenerateCli {
|
||||
parity: u8,
|
||||
}
|
||||
|
||||
/// Update FEC data after a file is modified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UpdateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to FEC data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
|
||||
/// Input file ranges that were updated.
|
||||
///
|
||||
/// This is a half-open range and can be specified multiple times.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
range: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Verify that a file contains no errors.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
@@ -137,6 +176,7 @@ struct RepairCli {
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum FecCommand {
|
||||
Generate(GenerateCli),
|
||||
Update(UpdateCli),
|
||||
Verify(VerifyCli),
|
||||
Repair(RepairCli),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::hashtree::HashTreeImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
fn read_hash_tree(path: &Path) -> Result<HashTreeImage> {
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let hash_tree = HashTreeImage::from_reader(reader)
|
||||
.with_context(|| format!("Failed to read hash tree data: {path:?}"))?;
|
||||
|
||||
Ok(hash_tree)
|
||||
}
|
||||
|
||||
fn write_hash_tree(path: &Path, hash_tree: &HashTreeImage) -> Result<()> {
|
||||
let mut writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
hash_tree
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write hash tree data: {path:?}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush hash tree data: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let salt = hex::decode(&cli.salt).context("Invalid salt")?;
|
||||
let input = open_input(&cli.input, false)?;
|
||||
|
||||
let hash_tree =
|
||||
HashTreeImage::generate(&input, cli.block_size, &cli.algorithm, &salt, cancel_signal)
|
||||
.context("Failed to generate hash tree data")?;
|
||||
|
||||
write_hash_tree(&cli.hash_tree, &hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let ranges = cli
|
||||
.range
|
||||
.chunks_exact(2)
|
||||
.map(|w| w[0]..w[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let mut hash_tree = read_hash_tree(&cli.hash_tree)?;
|
||||
|
||||
hash_tree
|
||||
.update(&input, &ranges, cancel_signal)
|
||||
.context("Failed to update hash tree data")?;
|
||||
|
||||
write_hash_tree(&cli.hash_tree, &hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let hash_tree = read_hash_tree(&cli.hash_tree)?;
|
||||
|
||||
hash_tree
|
||||
.verify(&input, cancel_signal)
|
||||
.context("Failed to verify data")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash_tree_main(cli: &HashTreeCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
HashTreeCommand::Generate(c) => generate_subcommand(c, cancel_signal),
|
||||
HashTreeCommand::Update(c) => update_subcommand(c, cancel_signal),
|
||||
HashTreeCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output hash tree data.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
|
||||
/// Block size.
|
||||
#[arg(short, long, value_name = "BYTES", default_value = "4096")]
|
||||
block_size: u32,
|
||||
|
||||
/// Hash algorithm.
|
||||
#[arg(short, long, value_name = "NAME", default_value = "sha256")]
|
||||
algorithm: String,
|
||||
|
||||
/// Salt (in hex).
|
||||
#[arg(short, long, value_name = "HEX", default_value = "")]
|
||||
salt: String,
|
||||
}
|
||||
|
||||
/// Update hash tree data after a file is modified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UpdateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to hash tree data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
|
||||
/// Input file ranges that were updated.
|
||||
///
|
||||
/// This is a half-open range and can be specified multiple times.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
range: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Verify that a file contains no errors.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input hash tree data.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum HashTreeCommand {
|
||||
Generate(GenerateCli),
|
||||
Update(UpdateCli),
|
||||
Verify(VerifyCli),
|
||||
}
|
||||
|
||||
/// Generate dm-verity hash tree data and verify files.
|
||||
///
|
||||
/// These commands operate on a standard hash tree data prepended by a custom
|
||||
/// header.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct HashTreeCli {
|
||||
#[command(subcommand)]
|
||||
command: HashTreeCommand,
|
||||
}
|
||||
@@ -71,6 +71,16 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
fs::write(&c.output, encoded)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::DecodeAvb(c) => {
|
||||
let encoded = fs::read(&c.key)
|
||||
.with_context(|| format!("Failed to load AVB public key: {:?}", c.key))?;
|
||||
|
||||
let public_key = avb::decode_public_key(&encoded)
|
||||
.context("Failed to decode public key as AVB format")?;
|
||||
|
||||
crypto::write_pem_public_key_file(&c.output, &public_key)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -152,11 +162,24 @@ struct ExtractAvbCli {
|
||||
passphrase: PassphraseGroup,
|
||||
}
|
||||
|
||||
/// Convert an AVB-encoded public key to a PKCS8-encoded public key.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DecodeAvbCli {
|
||||
/// Path to output PKCS8-encoded public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to AVB-encoded public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum KeyCommand {
|
||||
GenerateKey(GenerateKeyCli),
|
||||
GenerateCert(GenerateCertCli),
|
||||
ExtractAvb(ExtractAvbCli),
|
||||
DecodeAvb(DecodeAvbCli),
|
||||
}
|
||||
|
||||
/// Generate and convert keys.
|
||||
|
||||
+1
-15
@@ -9,20 +9,6 @@ pub mod boot;
|
||||
pub mod completion;
|
||||
pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod key;
|
||||
pub mod ota;
|
||||
|
||||
macro_rules! status {
|
||||
($($arg:tt)*) => {
|
||||
eprintln!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! warning {
|
||||
($($arg:tt)*) => {
|
||||
eprintln!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)*))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use status;
|
||||
pub(crate) use warning;
|
||||
|
||||
+617
-378
File diff suppressed because it is too large
Load Diff
+23
-3
@@ -164,6 +164,9 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
continue;
|
||||
} else if line.starts_with(b"-----BEGIN CERTIFICATE-----") {
|
||||
inside_base64 = true;
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
} else if line.starts_with(b"-----END CERTIFICATE-----") {
|
||||
inside_base64 = false;
|
||||
|
||||
@@ -173,13 +176,13 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
base64.clear();
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
} else if inside_base64 {
|
||||
base64.extend_from_slice(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
}
|
||||
|
||||
if inside_base64 {
|
||||
@@ -225,6 +228,23 @@ pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
write_pem_cert(writer, cert)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a writer.
|
||||
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key.to_public_key_pem(LineEnding::LF)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
|
||||
+103
-205
@@ -6,6 +6,7 @@
|
||||
use std::{
|
||||
cmp, fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
@@ -14,7 +15,6 @@ use bstr::ByteSlice;
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_bigint_dig::{ModInverse, ToBigInt};
|
||||
use num_traits::{Pow, ToPrimitive};
|
||||
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use rsa::{traits::PublicKeyParts, BigUint, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -25,6 +25,7 @@ use crate::{
|
||||
escape,
|
||||
format::{
|
||||
fec::{self, Fec},
|
||||
hashtree::{self, HashTree},
|
||||
padding,
|
||||
},
|
||||
stream::{
|
||||
@@ -35,7 +36,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub const VERSION_MAJOR: u32 = 1;
|
||||
pub const VERSION_MINOR: u32 = 2;
|
||||
pub const VERSION_MINOR: u32 = 3;
|
||||
pub const VERSION_SUB: u32 = 0;
|
||||
|
||||
pub const FOOTER_VERSION_MAJOR: u32 = 1;
|
||||
@@ -92,10 +93,6 @@ pub enum Error {
|
||||
},
|
||||
#[error("RSA key size (0) is not compatible with any AVB signing algorithm")]
|
||||
UnsupportedKey(usize),
|
||||
#[error("Expected root digest {expected}, but have {actual}")]
|
||||
InvalidRootDigest { expected: String, actual: String },
|
||||
#[error("Expected hash tree {expected}, but have {actual}")]
|
||||
InvalidHashTree { expected: String, actual: String },
|
||||
#[error("Hash tree does not immediately follow image data")]
|
||||
HashTreeGap,
|
||||
#[error("FEC data does not immediately follow hash tree")]
|
||||
@@ -112,6 +109,8 @@ pub enum Error {
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("{0} byte image size is too small to fit header or footer")]
|
||||
ImageSizeTooSmall(u64),
|
||||
#[error("Hash tree error")]
|
||||
HashTree(#[from] hashtree::Error),
|
||||
#[error("FEC error")]
|
||||
Fec(#[from] fec::Error),
|
||||
#[error("I/O error")]
|
||||
@@ -120,7 +119,7 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
|
||||
pub(crate) fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
|
||||
match name {
|
||||
"sha1" if for_verify => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
|
||||
"sha256" => Ok(&ring::digest::SHA256),
|
||||
@@ -382,156 +381,6 @@ impl HashTreeDescriptor {
|
||||
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
|
||||
pub const FLAG_CHECK_AT_MOST_ONCE: u32 = 1 << 1;
|
||||
|
||||
/// Calculate the hash tree digests for a single level of the tree. If the
|
||||
/// reader's position is block-aligned and `image_size` is a multiple of the
|
||||
/// block size, then this function can also be used to calculate the digests
|
||||
/// for a portion of a level.
|
||||
///
|
||||
/// NOTE: The result is **not** padded to the block size.
|
||||
fn hash_one_level(
|
||||
mut reader: impl Read,
|
||||
mut image_size: u64,
|
||||
block_size: u32,
|
||||
algorithm: &'static Algorithm,
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
// Each digest must be a power of 2.
|
||||
let digest_padding = algorithm.output_len().next_power_of_two() - algorithm.output_len();
|
||||
let mut buf = vec![0u8; block_size as usize];
|
||||
let mut result = vec![];
|
||||
|
||||
while image_size > 0 {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let n = image_size.min(buf.len() as u64) as usize;
|
||||
reader.read_exact(&mut buf[..n])?;
|
||||
|
||||
// For undersized blocks, we still hash the whole buffer, except
|
||||
// with padding.
|
||||
buf[n..].fill(0);
|
||||
|
||||
let mut context = Context::new(algorithm);
|
||||
context.update(salt);
|
||||
context.update(&buf);
|
||||
|
||||
// Add the digest to the tree level. Each tree node must be a power
|
||||
// of two.
|
||||
let digest = context.finish();
|
||||
result.extend(digest.as_ref());
|
||||
result.resize(result.len() + digest_padding, 0);
|
||||
|
||||
image_size -= n as u64;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Calls [`Self::hash_one_level()`] in parallel.
|
||||
///
|
||||
/// NOTE: The result is **not** padded to the block size.
|
||||
fn hash_one_level_parallel(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
block_size: u32,
|
||||
algorithm: &'static Algorithm,
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
assert!(
|
||||
image_size > block_size as u64,
|
||||
"Images smaller than block size must use a normal hash",
|
||||
);
|
||||
|
||||
// Parallelize in 16 MiB chunks to avoid too much seek thrashing.
|
||||
let chunk_size = padding::round(16 * 1024 * 1024, u64::from(block_size)).unwrap();
|
||||
let chunk_count = image_size / chunk_size + u64::from(image_size % chunk_size != 0);
|
||||
|
||||
let pieces = (0..chunk_count)
|
||||
.into_par_iter()
|
||||
.map(|c| -> io::Result<Vec<u8>> {
|
||||
let start = c * chunk_size;
|
||||
let size = chunk_size.min(image_size - start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(start))?;
|
||||
|
||||
Self::hash_one_level(reader, size, block_size, algorithm, salt, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(pieces.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
/// Calculate the hash tree for the given input in parallel.
|
||||
fn calculate_hash_tree(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
block_size: u32,
|
||||
algorithm: &'static Algorithm,
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<(Vec<u8>, Vec<u8>)> {
|
||||
// Small files are hashed directly, exactly like a hash descriptor.
|
||||
if image_size <= u64::from(block_size) {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; block_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut context = Context::new(algorithm);
|
||||
context.update(salt);
|
||||
context.update(&buf);
|
||||
let digest = context.finish();
|
||||
|
||||
return Ok((digest.as_ref().to_vec(), vec![]));
|
||||
}
|
||||
|
||||
// Large files use the hash tree.
|
||||
let mut levels = Vec::<Vec<u8>>::new();
|
||||
let mut level_size = image_size;
|
||||
|
||||
while level_size > u64::from(block_size) {
|
||||
let mut level = if let Some(prev_level) = levels.last() {
|
||||
// Hash the previous level.
|
||||
Self::hash_one_level(
|
||||
Cursor::new(prev_level),
|
||||
level_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
salt,
|
||||
cancel_signal,
|
||||
)?
|
||||
} else {
|
||||
// Initially read from file.
|
||||
Self::hash_one_level_parallel(
|
||||
input,
|
||||
level_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
salt,
|
||||
cancel_signal,
|
||||
)?
|
||||
};
|
||||
|
||||
// Pad to the block size.
|
||||
level.resize(padding::round(level.len(), block_size as usize).unwrap(), 0);
|
||||
|
||||
level_size = level.len() as u64;
|
||||
levels.push(level);
|
||||
}
|
||||
|
||||
// Calculate the root hash.
|
||||
let mut context = Context::new(algorithm);
|
||||
context.update(salt);
|
||||
context.update(levels.last().unwrap());
|
||||
let root_hash = context.finish().as_ref().to_vec();
|
||||
|
||||
// The tree is oriented such that the leaves are at the end.
|
||||
let hash_tree = levels.into_iter().rev().flatten().collect();
|
||||
|
||||
Ok((root_hash, hash_tree))
|
||||
}
|
||||
|
||||
/// Ensure that the image data is immediately followed by the hash tree and
|
||||
/// then the FEC data.
|
||||
fn check_offsets(&self) -> Result<()> {
|
||||
@@ -585,10 +434,15 @@ impl HashTreeDescriptor {
|
||||
/// the same file. It is guaranteed that every thread will read and write
|
||||
/// disjoint file offsets.
|
||||
///
|
||||
/// Due to the nature of the file access patterns needed to generate the
|
||||
/// hash tree and FEC data, the entire file will be read twice. However, if
|
||||
/// If `ranges` is [`Option::None`], then the hash tree and FEC data are
|
||||
/// updated for the whole while. Due to the nature of the file access
|
||||
/// patterns, the entire file will be read twice. However, if
|
||||
/// [`Self::fec_num_roots`] is 0, no FEC data will be computed nor written.
|
||||
///
|
||||
/// If `ranges` is specified, only the hash tree and FEC data corresponding
|
||||
/// to those ranges are updated. It may be necessary read a bit more data
|
||||
/// that what is specified in order to perform the computations.
|
||||
///
|
||||
/// The fields in this instance are updated atomically. No fields are
|
||||
/// updated if an error occurs. The input file can be restored back to its
|
||||
/// original state by truncating it to [`Self::image_size`].
|
||||
@@ -596,28 +450,44 @@ impl HashTreeDescriptor {
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, false)?;
|
||||
let (root_digest, hash_tree) = Self::calculate_hash_tree(
|
||||
input,
|
||||
self.image_size,
|
||||
self.data_block_size,
|
||||
algorithm,
|
||||
&self.salt,
|
||||
cancel_signal,
|
||||
)?;
|
||||
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
|
||||
let (root_digest, hash_tree_data) = match ranges {
|
||||
Some(r) => {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(self.tree_offset))?;
|
||||
|
||||
if hash_tree.len() > HASH_TREE_MAX_SIZE as usize {
|
||||
let mut hash_tree_data = vec![0u8; self.tree_size as usize];
|
||||
reader
|
||||
.read_exact(&mut hash_tree_data)
|
||||
.map_err(|e| Error::ReadFieldError("hash_tree", e))?;
|
||||
|
||||
let root_digest = hash_tree.update(
|
||||
input,
|
||||
self.image_size,
|
||||
r,
|
||||
&mut hash_tree_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
(root_digest, hash_tree_data)
|
||||
}
|
||||
None => hash_tree.generate(input, self.image_size, cancel_signal)?,
|
||||
};
|
||||
|
||||
if hash_tree_data.len() > HASH_TREE_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("tree_size"));
|
||||
}
|
||||
|
||||
let tree_size = hash_tree.len() as u64;
|
||||
let tree_size = hash_tree_data.len() as u64;
|
||||
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(self.image_size))?;
|
||||
writer
|
||||
.write_all(&hash_tree)
|
||||
.write_all(&hash_tree_data)
|
||||
.map_err(|e| Error::WriteFieldError("hash_tree", e))?;
|
||||
|
||||
// The FEC data section is optional.
|
||||
@@ -634,10 +504,31 @@ impl HashTreeDescriptor {
|
||||
.to_u8()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_num_roots"))?;
|
||||
|
||||
// The FEC covers the hash tree as well.
|
||||
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)?;
|
||||
let fec_data = match ranges {
|
||||
Some(r) => {
|
||||
let mut r_with_hash_tree = r.to_vec();
|
||||
r_with_hash_tree.push(self.tree_offset..self.tree_offset + tree_size);
|
||||
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
let (fec, fec_size) = self.get_fec()?;
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(self.fec_offset))?;
|
||||
|
||||
let mut fec_data = vec![0u8; fec_size];
|
||||
reader
|
||||
.read_exact(&mut fec_data)
|
||||
.map_err(|e| Error::ReadFieldError("fec_data", e))?;
|
||||
|
||||
fec.update(input, &r_with_hash_tree, &mut fec_data, cancel_signal)?;
|
||||
|
||||
fec_data
|
||||
}
|
||||
None => {
|
||||
// The FEC covers the hash tree as well.
|
||||
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)?;
|
||||
fec.generate(input, cancel_signal)?
|
||||
}
|
||||
};
|
||||
let fec_size = fec_data
|
||||
.len()
|
||||
.to_u64()
|
||||
@@ -675,40 +566,23 @@ impl HashTreeDescriptor {
|
||||
return Err(Error::FieldOutOfBounds("tree_size"));
|
||||
}
|
||||
|
||||
let (actual_root_digest, actual_hash_tree) = Self::calculate_hash_tree(
|
||||
input,
|
||||
self.image_size,
|
||||
self.data_block_size,
|
||||
algorithm,
|
||||
&self.salt,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
if self.root_digest != actual_root_digest {
|
||||
return Err(Error::InvalidRootDigest {
|
||||
expected: hex::encode(&self.root_digest),
|
||||
actual: hex::encode(actual_root_digest),
|
||||
});
|
||||
}
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(self.tree_offset))?;
|
||||
|
||||
let mut hash_tree = vec![0u8; self.tree_size as usize];
|
||||
let mut hash_tree_data = vec![0u8; self.tree_size as usize];
|
||||
reader
|
||||
.read_exact(&mut hash_tree)
|
||||
.read_exact(&mut hash_tree_data)
|
||||
.map_err(|e| Error::ReadFieldError("hash_tree", e))?;
|
||||
|
||||
if hash_tree != actual_hash_tree {
|
||||
// These are multiple megabytes, so only report the hashes.
|
||||
let expected = ring::digest::digest(algorithm, &hash_tree);
|
||||
let actual = ring::digest::digest(algorithm, &actual_hash_tree);
|
||||
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
|
||||
|
||||
return Err(Error::InvalidHashTree {
|
||||
expected: hex::encode(expected),
|
||||
actual: hex::encode(actual),
|
||||
});
|
||||
}
|
||||
hash_tree.verify(
|
||||
input,
|
||||
self.image_size,
|
||||
&self.root_digest,
|
||||
&hash_tree_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
// The FEC data section is optional.
|
||||
if self.fec_num_roots != 0 {
|
||||
@@ -945,10 +819,11 @@ impl HashDescriptor {
|
||||
let digest = self.calculate(reader, true, cancel_signal)?;
|
||||
|
||||
if self.root_digest != digest.as_ref() {
|
||||
return Err(Error::InvalidRootDigest {
|
||||
return Err(hashtree::Error::InvalidRootDigest {
|
||||
expected: hex::encode(&self.root_digest),
|
||||
actual: hex::encode(digest),
|
||||
});
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1109,8 +984,13 @@ pub struct ChainPartitionDescriptor {
|
||||
pub partition_name: String,
|
||||
#[serde(with = "hex")]
|
||||
pub public_key: Vec<u8>,
|
||||
pub flags: u32,
|
||||
#[serde(with = "hex")]
|
||||
pub reserved: [u8; 64],
|
||||
pub reserved: [u8; 60],
|
||||
}
|
||||
|
||||
impl ChainPartitionDescriptor {
|
||||
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChainPartitionDescriptor {
|
||||
@@ -1119,6 +999,7 @@ impl fmt::Debug for ChainPartitionDescriptor {
|
||||
.field("rollback_index_location", &self.rollback_index_location)
|
||||
.field("partition_name", &self.partition_name)
|
||||
.field("public_key", &hex::encode(&self.public_key))
|
||||
.field("flags", &self.flags)
|
||||
.field("reserved", &hex::encode(self.reserved))
|
||||
.finish()
|
||||
}
|
||||
@@ -1142,7 +1023,9 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
|
||||
return Err(Error::FieldOutOfBounds("public_key_len"));
|
||||
}
|
||||
|
||||
let mut reserved = [0u8; 64];
|
||||
let flags = reader.read_u32::<BigEndian>()?;
|
||||
|
||||
let mut reserved = [0u8; 60];
|
||||
reader.read_exact(&mut reserved)?;
|
||||
|
||||
// Not NULL-terminated.
|
||||
@@ -1157,6 +1040,7 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
|
||||
rollback_index_location,
|
||||
partition_name,
|
||||
public_key,
|
||||
flags,
|
||||
reserved,
|
||||
};
|
||||
|
||||
@@ -1177,6 +1061,7 @@ impl<W: Write> ToWriter<W> for ChainPartitionDescriptor {
|
||||
writer.write_u32::<BigEndian>(self.rollback_index_location)?;
|
||||
writer.write_u32::<BigEndian>(self.partition_name.len() as u32)?;
|
||||
writer.write_u32::<BigEndian>(self.public_key.len() as u32)?;
|
||||
writer.write_u32::<BigEndian>(self.flags)?;
|
||||
writer.write_all(&self.reserved)?;
|
||||
writer.write_all(self.partition_name.as_bytes())?;
|
||||
writer.write_all(&self.public_key)?;
|
||||
@@ -1962,7 +1847,20 @@ fn write_image_internal(
|
||||
image_size: Option<u64>,
|
||||
block_size: u64,
|
||||
) -> Result<()> {
|
||||
let eof_image_size = writer.seek(SeekFrom::End(0))?;
|
||||
let eof_image_size = if footer.is_some() {
|
||||
match header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => d
|
||||
.image_size
|
||||
.checked_add(d.tree_size)
|
||||
.and_then(|s| s.checked_add(d.fec_size))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("eof_image_size"))?,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
writer.seek(SeekFrom::Start(eof_image_size))?;
|
||||
|
||||
// The header must be block-aligned.
|
||||
let vbmeta_offset = if block_size > 0 {
|
||||
|
||||
@@ -7,17 +7,25 @@ use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
@@ -102,25 +110,29 @@ pub enum CompressedFormat {
|
||||
None,
|
||||
Gzip,
|
||||
Lz4Legacy,
|
||||
Xz,
|
||||
}
|
||||
|
||||
pub enum CompressedReader<R: Read> {
|
||||
None(R),
|
||||
Gzip(GzDecoder<R>),
|
||||
Lz4(FrameDecoder<R>),
|
||||
Xz(XzDecoder<R>),
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||
let mut magic = [0u8; 4];
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
reader.rewind()?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
} else if &magic == LZ4_LEGACY_MAGIC {
|
||||
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
|
||||
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
||||
} else if &magic == XZ_MAGIC {
|
||||
Ok(Self::Xz(XzDecoder::new(reader)))
|
||||
} else if raw_if_unknown {
|
||||
Ok(Self::None(reader))
|
||||
} else {
|
||||
@@ -133,6 +145,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +154,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
Self::None(r) => r,
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +165,7 @@ impl<R: Read> Read for CompressedReader<R> {
|
||||
Self::None(r) => r.read(buf),
|
||||
Self::Gzip(r) => r.read(buf),
|
||||
Self::Lz4(r) => r.read(buf),
|
||||
Self::Xz(r) => r.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +174,7 @@ pub enum CompressedWriter<W: Write> {
|
||||
None(W),
|
||||
Gzip(GzEncoder<W>),
|
||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||
Xz(XzEncoder<W>),
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedWriter<W> {
|
||||
@@ -169,6 +185,11 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +198,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +207,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::None(w) => Ok(w),
|
||||
Self::Gzip(w) => w.finish(),
|
||||
Self::Lz4Legacy(w) => w.finish(),
|
||||
Self::Xz(w) => w.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +218,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::None(w) => w.write(buf),
|
||||
Self::Gzip(w) => w.write(buf),
|
||||
Self::Lz4Legacy(w) => w.write(buf),
|
||||
Self::Xz(w) => w.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +227,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::None(w) => w.flush(),
|
||||
Self::Gzip(w) => w.flush(),
|
||||
Self::Lz4Legacy(w) => w.flush(),
|
||||
Self::Xz(w) => w.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
-13
@@ -4,8 +4,10 @@
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -20,7 +22,7 @@ use thiserror::Error;
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
util::NumBytes,
|
||||
util::{self, NumBytes},
|
||||
};
|
||||
|
||||
// Not to be confused with the 255-byte RS block size.
|
||||
@@ -111,11 +113,6 @@ impl Codeword {
|
||||
}
|
||||
}
|
||||
|
||||
/// Since Rust's built-in .div_ceil() is still nightly-only.
|
||||
fn div_ceil(dividend: u64, divisor: u64) -> u64 {
|
||||
dividend / divisor + u64::from(dividend % divisor != 0)
|
||||
}
|
||||
|
||||
/// A type for performing FEC generation, verification, and error correction for
|
||||
/// a specific file size and Reed Solomon configuration. The implementation uses
|
||||
/// dm-verity's interleaving access pattern.
|
||||
@@ -169,8 +166,8 @@ impl Fec {
|
||||
return Err(Error::UnsupportedParity(parity));
|
||||
}
|
||||
|
||||
let blocks = div_ceil(file_size, u64::from(block_size));
|
||||
let rounds = div_ceil(blocks, u64::from(rs_k));
|
||||
let blocks = util::div_ceil(file_size, u64::from(block_size));
|
||||
let rounds = util::div_ceil(blocks, u64::from(rs_k));
|
||||
|
||||
// Check upfront so we don't need to do checked multiplication later.
|
||||
rounds
|
||||
@@ -211,6 +208,34 @@ impl Fec {
|
||||
offset / rs_k + offset % rs_k * self.rounds * u64::from(self.block_size)
|
||||
}
|
||||
|
||||
/// Get the rounds that correspond to the specified ranges.
|
||||
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > self.file_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
let mut result = HashSet::new();
|
||||
|
||||
for range in ranges {
|
||||
let start_block = range.start / block_size;
|
||||
let end_block = if range.end % block_size == 0 {
|
||||
range.end / block_size
|
||||
} else {
|
||||
util::div_ceil(range.end, block_size)
|
||||
};
|
||||
|
||||
for block in start_block..end_block {
|
||||
result.insert(block % self.rounds);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Read a raw sequential block from the backing file, starting at offset
|
||||
/// `offset` in the interleaved view. This reads a horizontal block-aligned
|
||||
/// slice in the file offset grid.
|
||||
@@ -438,6 +463,41 @@ impl Fec {
|
||||
Ok(fec)
|
||||
}
|
||||
|
||||
/// Update FEC data coreesponding to the specified file ranges.
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn update(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
fec: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec_size = self.fec_size();
|
||||
if fec.len() != fec_size {
|
||||
return Err(Error::InvalidFecSize {
|
||||
input: self.file_size,
|
||||
expected: fec_size,
|
||||
actual: fec.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let rounds_to_update = self.rounds_for_ranges(ranges)?;
|
||||
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify that the file contains no errors. This is significantly faster
|
||||
/// than [`Self::repair()`] if only error detection, not correction, is
|
||||
/// needed.
|
||||
@@ -530,7 +590,7 @@ pub struct FecImage {
|
||||
|
||||
impl fmt::Debug for FecImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Fec")
|
||||
f.debug_struct("FecImage")
|
||||
.field("fec", &NumBytes(self.fec.len()))
|
||||
.field("data_size", &self.data_size)
|
||||
.field("parity", &self.parity)
|
||||
@@ -560,6 +620,17 @@ impl FecImage {
|
||||
})
|
||||
}
|
||||
|
||||
/// Update FEC data coreesponding to the specified file ranges.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.update(input, ranges, &mut self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Check that a file contains no errors. This is significantly faster than
|
||||
/// [`Self::repair()`] if performing a repair is not necessary.
|
||||
pub fn verify(
|
||||
@@ -731,6 +802,31 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rounds_for_ranges() {
|
||||
let size = 2 * 253 * 4096;
|
||||
let fec = Fec::new(size, 4096, 2).unwrap();
|
||||
|
||||
assert_eq!(fec.rounds_for_ranges(&[0..0]).unwrap(), HashSet::new());
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[0..size]).unwrap(),
|
||||
HashSet::from([0, 1]),
|
||||
);
|
||||
assert_eq!(fec.rounds_for_ranges(&[0..1]).unwrap(), HashSet::from([0]));
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[4095..4096]).unwrap(),
|
||||
HashSet::from([0]),
|
||||
);
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[4095..4097]).unwrap(),
|
||||
HashSet::from([0, 1]),
|
||||
);
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[size - 1..size]).unwrap(),
|
||||
HashSet::from([1]),
|
||||
);
|
||||
}
|
||||
|
||||
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
|
||||
let mut buf = [0u8; 1];
|
||||
|
||||
@@ -781,7 +877,9 @@ mod tests {
|
||||
corrupt_byte(&mut file, offset as u64);
|
||||
}
|
||||
|
||||
// Verify that all the single-byte errors can be fixed.
|
||||
// Verify that all the single-byte errors can be fixed. We don't test
|
||||
// for Error::TooManyErrors because of the chance of false positives due
|
||||
// to the nature of RS.
|
||||
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
let repaired_digest = {
|
||||
@@ -792,12 +890,17 @@ mod tests {
|
||||
};
|
||||
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
|
||||
|
||||
// We don't test for Error::TooManyErrors because of the chance of false
|
||||
// positives due to the nature of RS.
|
||||
// Intentionally update some data.
|
||||
corrupt_byte(&mut file, 0);
|
||||
let mut fec_data_updated = fec_data.clone();
|
||||
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
|
||||
fec.update(&file, &[0..1], &mut fec_data_updated, &cancel_signal)
|
||||
.unwrap();
|
||||
assert_eq!(fec_data_updated, fec_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_verify_repair() {
|
||||
fn generate_update_verify_repair() {
|
||||
for block_size in [1, 2, 4, 8, 16, 32, 64] {
|
||||
for rs_k in verityrs::FN_ENCODE.keys() {
|
||||
println!("Testing block_size={block_size}, rs_k={rs_k}");
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
format::{avb, padding},
|
||||
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
|
||||
util::{self, NumBytes},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Hash tree should have size {expected} for input size {input}, but has size {actual}")]
|
||||
InvalidHashTreeSize {
|
||||
input: u64,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("Expected root digest {expected}, but have {actual}")]
|
||||
InvalidRootDigest { expected: String, actual: String },
|
||||
#[error("Expected hash tree {expected}, but have {actual}")]
|
||||
InvalidHashTree { expected: String, actual: String },
|
||||
#[error("Invalid hash tree header magic: {:?}", .0.as_bstr())]
|
||||
InvalidHeaderMagic([u8; 16]),
|
||||
#[error("Invalid hash tree header version: {0}")]
|
||||
InvalidHeaderVersion(u16),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct HashTree {
|
||||
block_size: u32,
|
||||
salted_context: Context,
|
||||
}
|
||||
|
||||
impl HashTree {
|
||||
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &[u8]) -> Self {
|
||||
let mut salted_context = Context::new(algorithm);
|
||||
salted_context.update(salt);
|
||||
|
||||
Self {
|
||||
block_size,
|
||||
salted_context,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of offset ranges that each level occupies in the hash
|
||||
/// tree data. The items are returned with the bottom level's offsets first
|
||||
/// in the list. Note that the bottom level is stored at the end of the hash
|
||||
/// tree data.
|
||||
fn compute_level_offsets(&self, image_size: u64) -> Result<Vec<Range<usize>>> {
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
let mut ranges = vec![];
|
||||
let mut level_size = image_size;
|
||||
|
||||
while level_size > u64::from(self.block_size) {
|
||||
let blocks = util::div_ceil(level_size, u64::from(self.block_size));
|
||||
level_size = blocks
|
||||
.checked_mul(digest_size as u64)
|
||||
.and_then(|s| padding::round(s, u64::from(self.block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
|
||||
// Depending on the chosen block size, the original file size could
|
||||
// overflow a usize without the first level's size doing the same.
|
||||
let level_size_usize = level_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
|
||||
ranges.push(0..level_size_usize);
|
||||
}
|
||||
|
||||
// The hash tree puts the leaves at the end.
|
||||
let mut offset = 0;
|
||||
for range in ranges.iter_mut().rev() {
|
||||
let level_size = range.end - range.start;
|
||||
range.start += offset;
|
||||
range.end += offset;
|
||||
offset += level_size;
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
/// Convert a list of ranges of byte offsets to a sorted, non-overlapping
|
||||
/// list of block ranges.
|
||||
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > image_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
let mut result = Vec::new();
|
||||
|
||||
for range in ranges {
|
||||
let start_block = range.start / block_size;
|
||||
let end_block = if range.end % block_size == 0 {
|
||||
range.end / block_size
|
||||
} else {
|
||||
util::div_ceil(range.end, block_size)
|
||||
};
|
||||
|
||||
result.push(start_block..end_block);
|
||||
}
|
||||
|
||||
Ok(util::merge_overlapping(&result))
|
||||
}
|
||||
|
||||
/// Calculate the hash tree digests for a single level of the tree. If the
|
||||
/// reader's position is block-aligned and `image_size` is a multiple of the
|
||||
/// block size, then this function can also be used to calculate the digests
|
||||
/// for a portion of a level.
|
||||
fn hash_partial_level(
|
||||
&self,
|
||||
mut reader: impl Read,
|
||||
mut size: u64,
|
||||
mut level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
// Each digest must be a power of 2.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_padding = algorithm.output_len().next_power_of_two() - algorithm.output_len();
|
||||
let mut buf = vec![0u8; self.block_size as usize];
|
||||
|
||||
while size > 0 {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let n = size.min(buf.len() as u64) as usize;
|
||||
reader.read_exact(&mut buf[..n])?;
|
||||
|
||||
// For undersized blocks, we still hash the whole buffer, except
|
||||
// with padding.
|
||||
buf[n..].fill(0);
|
||||
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
|
||||
// Add the digest to the tree level. Each tree node must be a power
|
||||
// of two.
|
||||
let digest = context.finish();
|
||||
|
||||
level_data[..digest.as_ref().len()].copy_from_slice(digest.as_ref());
|
||||
level_data = &mut level_data[digest.as_ref().len()..];
|
||||
|
||||
level_data[..digest_padding].fill(0);
|
||||
level_data = &mut level_data[digest_padding..];
|
||||
|
||||
size -= n as u64;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hash one full level in parallel.
|
||||
fn hash_one_level_parallel(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
size: u64,
|
||||
level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
assert!(
|
||||
size > self.block_size as u64,
|
||||
"Images smaller than block size must use a normal hash",
|
||||
);
|
||||
|
||||
// Parallelize in larger chunks to avoid too much seek thrashing.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
let multiplier = 1024u64;
|
||||
|
||||
level_data
|
||||
.par_chunks_mut(digest_size * multiplier as usize)
|
||||
.enumerate()
|
||||
.map(|(chunk, out_data)| -> io::Result<()> {
|
||||
let digests = out_data.len() / digest_size;
|
||||
let in_start = (chunk as u64) * multiplier * u64::from(self.block_size);
|
||||
let in_size = ((digests as u64) * u64::from(self.block_size)).min(size - in_start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(in_start))?;
|
||||
|
||||
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update parts of the hash tree level corresponding to the specified
|
||||
/// blocks.
|
||||
fn hash_partial_level_parallel(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
size: u64,
|
||||
block_ranges: &[Range<u64>],
|
||||
level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
|
||||
level_data
|
||||
.par_chunks_exact_mut(digest_size)
|
||||
.enumerate()
|
||||
.filter(|(chunk, _)| util::ranges_contains(block_ranges, &(*chunk as u64)))
|
||||
.map(|(chunk, out_data)| -> io::Result<()> {
|
||||
let in_start = (chunk as u64) * u64::from(self.block_size);
|
||||
let in_size = u64::from(self.block_size).min(size - in_start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(in_start))?;
|
||||
|
||||
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the hash tree and return the root digest. If `ranges` is
|
||||
/// specified, then only the input file blocks containing those ranges are
|
||||
/// recomputed.
|
||||
///
|
||||
/// `hash_tree_data` must match `level_offsets`. In other words, the ending
|
||||
/// offset of the leaf layer of the tree must equal `hash_tree_data`'s size.
|
||||
fn calculate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
level_offsets: &[Range<usize>],
|
||||
hash_tree_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
// Small files are hashed directly.
|
||||
if image_size <= u64::from(self.block_size) {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; image_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
let digest = context.finish();
|
||||
|
||||
return Ok(digest.as_ref().to_vec());
|
||||
}
|
||||
|
||||
// Large files use the hash tree.
|
||||
for (i, level_range) in level_offsets.iter().enumerate() {
|
||||
let (front, back) = hash_tree_data.split_at_mut(level_range.end);
|
||||
let level_data = &mut front[level_range.clone()];
|
||||
|
||||
if i > 0 {
|
||||
// Hash the previous level.
|
||||
let prev_range = level_offsets[i - 1].clone();
|
||||
let prev_size = prev_range.end - prev_range.start;
|
||||
let prev_data = &back[..prev_size];
|
||||
|
||||
self.hash_partial_level(
|
||||
Cursor::new(prev_data),
|
||||
prev_size as u64,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
} else if let Some(r) = ranges {
|
||||
// Read partial blocks from file.
|
||||
let block_ranges = self.blocks_for_ranges(image_size, r)?;
|
||||
|
||||
self.hash_partial_level_parallel(
|
||||
input,
|
||||
image_size,
|
||||
&block_ranges,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
} else {
|
||||
// Read entire file.
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
|
||||
}
|
||||
|
||||
// No need to explicitly ensure the level is padded to the block
|
||||
// size since the tree is initialized with zeros.
|
||||
}
|
||||
|
||||
// Calculate the root hash.
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&hash_tree_data[level_offsets.last().unwrap().clone()]);
|
||||
let root_hash = context.finish().as_ref().to_vec();
|
||||
|
||||
Ok(root_hash)
|
||||
}
|
||||
|
||||
/// Generate hash tree data for the file. Returns the root digest and the
|
||||
/// hash tree data.
|
||||
pub fn generate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let mut hash_tree_data = vec![0u8; hash_tree_size];
|
||||
|
||||
let root_digest = self.calculate(
|
||||
input,
|
||||
image_size,
|
||||
None,
|
||||
&offsets,
|
||||
&mut hash_tree_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok((root_digest, hash_tree_data))
|
||||
}
|
||||
|
||||
/// Update hash tree data corresponding to the specified file ranges.
|
||||
/// Returns the new root digest.
|
||||
pub fn update(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
ranges: &[Range<u64>],
|
||||
hash_tree_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
expected: hash_tree_size,
|
||||
actual: hash_tree_data.len(),
|
||||
});
|
||||
}
|
||||
|
||||
self.calculate(
|
||||
input,
|
||||
image_size,
|
||||
Some(ranges),
|
||||
&offsets,
|
||||
hash_tree_data,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify that the file contains no errors.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
root_digest: &[u8],
|
||||
hash_tree_data: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
expected: hash_tree_size,
|
||||
actual: hash_tree_data.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let (actual_root_digest, actual_hash_tree_data) =
|
||||
self.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
if root_digest != actual_root_digest {
|
||||
return Err(Error::InvalidRootDigest {
|
||||
expected: hex::encode(root_digest),
|
||||
actual: hex::encode(&actual_root_digest),
|
||||
});
|
||||
}
|
||||
|
||||
if hash_tree_data != actual_hash_tree_data {
|
||||
// These are multiple megabytes, so only report the hashes.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let expected = ring::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = ring::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
|
||||
return Err(Error::InvalidHashTree {
|
||||
expected: hex::encode(expected),
|
||||
actual: hex::encode(actual),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for reading and writing a custom hash tree image format.
|
||||
///
|
||||
/// File format:
|
||||
/// - [0 .. 16] - ASCII - "avbroot!hashtree"
|
||||
/// - [16 .. 18] - U16LE - Version
|
||||
/// - [18 .. 26] - U64LE - Image size
|
||||
/// - [26 .. 30] - U32LE - Block size
|
||||
/// - [30 .. 46] - ASCII - Hash algorithm
|
||||
/// - [46 .. 48] - U16LE - Salt size
|
||||
/// - [48 .. 50] - U16LE - Root digest size
|
||||
/// - [50 .. 54] - U32LE - Hash tree size
|
||||
/// - [<variable>] - BINARY - Salt
|
||||
/// - [<variable>] - BINARY - Root digest
|
||||
/// - [<variable>] - BINARY - Hash tree
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct HashTreeImage {
|
||||
pub image_size: u64,
|
||||
pub block_size: u32,
|
||||
pub algorithm: String,
|
||||
pub salt: Vec<u8>,
|
||||
pub root_digest: Vec<u8>,
|
||||
pub hash_tree: Vec<u8>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for HashTreeImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HashTreeImage")
|
||||
.field("block_size", &self.block_size)
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("salt", &hex::encode(&self.salt))
|
||||
.field("root_digest", &hex::encode(&self.root_digest))
|
||||
.field("hash_tree", &NumBytes(self.hash_tree.len()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HashTreeImage {
|
||||
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::ring_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
pub fn generate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
block_size: u32,
|
||||
algorithm: &str,
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let image_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let ring_algorithm = Self::ring_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm: algorithm.to_owned(),
|
||||
salt: salt.to_vec(),
|
||||
root_digest,
|
||||
hash_tree: hash_tree_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update hash tree data coreesponding to the specified file ranges.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
|
||||
self.root_digest = hash_tree.update(
|
||||
input,
|
||||
self.image_size,
|
||||
ranges,
|
||||
&mut self.hash_tree,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that a file contains no errors.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
|
||||
hash_tree.verify(
|
||||
input,
|
||||
self.image_size,
|
||||
&self.root_digest,
|
||||
&self.hash_tree,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
let mut magic = [0u8; 16];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u16::<LittleEndian>()?;
|
||||
if version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(version));
|
||||
}
|
||||
|
||||
let image_size = reader.read_u64::<LittleEndian>()?;
|
||||
let block_size = reader.read_u32::<LittleEndian>()?;
|
||||
let algorithm = reader.read_string_padded(16)?;
|
||||
let salt_size = reader.read_u16::<LittleEndian>()?;
|
||||
let root_digest_size = reader.read_u16::<LittleEndian>()?;
|
||||
let hash_tree_size = reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
|
||||
let mut salt = vec![0u8; usize::from(salt_size)];
|
||||
reader.read_exact(&mut salt)?;
|
||||
|
||||
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
|
||||
reader.read_exact(&mut root_digest)?;
|
||||
|
||||
let mut hash_tree = vec![0u8; hash_tree_size];
|
||||
reader.read_exact(&mut hash_tree)?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
salt,
|
||||
root_digest,
|
||||
hash_tree,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let salt_size = self
|
||||
.salt
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
|
||||
let root_digest_size = self
|
||||
.root_digest
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
|
||||
let hash_tree_size = self
|
||||
.hash_tree
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
|
||||
writer.write_all(Self::MAGIC)?;
|
||||
writer.write_u16::<LittleEndian>(Self::VERSION)?;
|
||||
writer.write_u64::<LittleEndian>(self.image_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.block_size)?;
|
||||
writer.write_string_padded(&self.algorithm, 16)?;
|
||||
writer.write_u16::<LittleEndian>(salt_size)?;
|
||||
writer.write_u16::<LittleEndian>(root_digest_size)?;
|
||||
writer.write_u32::<LittleEndian>(hash_tree_size)?;
|
||||
writer.write_all(&self.salt)?;
|
||||
writer.write_all(&self.root_digest)?;
|
||||
writer.write_all(&self.hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::stream::SharedCursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn calculate_level_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, &[]);
|
||||
assert_eq!(
|
||||
hash_tree.compute_level_offsets(0).unwrap(),
|
||||
&[] as &[Range<usize>],
|
||||
);
|
||||
assert_eq!(
|
||||
hash_tree.compute_level_offsets(1024 * 1024 * 1024).unwrap(),
|
||||
&[69632..8458240, 4096..69632, 0..4096],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_for_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, b"Salt");
|
||||
assert_eq!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16384]).unwrap(),
|
||||
&[0..4],
|
||||
);
|
||||
assert_eq!(hash_tree.blocks_for_ranges(16384, &[0..0]).unwrap(), &[]);
|
||||
assert_eq!(
|
||||
hash_tree
|
||||
.blocks_for_ranges(16384, &[12287..12289, 0..1, 5000..5001])
|
||||
.unwrap(),
|
||||
&[0..4],
|
||||
);
|
||||
assert_matches!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16385]),
|
||||
Err(Error::FieldOutOfBounds(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_update_verify() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
|
||||
let mut input = SharedCursor::new();
|
||||
|
||||
// Try input smaller than one block.
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
root_digest,
|
||||
&[
|
||||
0x15, 0x0f, 0xe5, 0x51, 0x40, 0x30, 0xb1, 0x43, 0x4a, 0x5d, 0xea, 0xf4, 0x91, 0xec,
|
||||
0xe9, 0x2c, 0x0e, 0x64, 0x97, 0x44, 0x7d, 0x6d, 0xe7, 0xbd, 0x6b, 0xa8, 0x5e, 0x8c,
|
||||
0xae, 0x1e, 0x00, 0xa3
|
||||
],
|
||||
);
|
||||
assert_eq!(hash_tree_data, &[]);
|
||||
|
||||
// Try larger input that spans multiple blocks are results in an actual
|
||||
// hash tree being created.
|
||||
input.write_all(&b"Data".repeat(25)).unwrap();
|
||||
|
||||
let (root_digest, mut hash_tree_data) =
|
||||
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
|
||||
assert_eq!(
|
||||
root_digest,
|
||||
&[
|
||||
0x92, 0xc3, 0xd7, 0x4a, 0x64, 0x03, 0x4b, 0xcc, 0xa9, 0x9a, 0x44, 0xf6, 0x81, 0xa2,
|
||||
0x4d, 0xdd, 0x97, 0xd3, 0xda, 0x84, 0xdc, 0xe2, 0x1b, 0x83, 0xd1, 0x7b, 0xab, 0x60,
|
||||
0x59, 0xe8, 0x45, 0x59
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
hash_tree_data,
|
||||
&[
|
||||
0x7e, 0x33, 0x47, 0xb6, 0xf3, 0x7c, 0xde, 0x0e, 0xe2, 0x8d, 0x9e, 0x49, 0x8e, 0xd4,
|
||||
0xbd, 0x53, 0x3a, 0xa1, 0xff, 0xeb, 0x4f, 0x6d, 0x5a, 0x5f, 0x55, 0x28, 0x37, 0x79,
|
||||
0xd0, 0x25, 0x07, 0xd5, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
|
||||
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
|
||||
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
|
||||
],
|
||||
);
|
||||
|
||||
// Change some data and update the hash tree.
|
||||
input.rewind().unwrap();
|
||||
input.write_all(b"Changed").unwrap();
|
||||
|
||||
let root_digest = hash_tree
|
||||
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
root_digest,
|
||||
&[
|
||||
0x8d, 0x03, 0xad, 0x18, 0xf2, 0x53, 0x13, 0x59, 0xf5, 0xbf, 0x68, 0x0e, 0x0c, 0x4a,
|
||||
0x86, 0xe2, 0x6e, 0xaa, 0x3d, 0x4b, 0x0f, 0x1b, 0x57, 0xad, 0x92, 0xe7, 0xbf, 0x3e,
|
||||
0xa6, 0xb1, 0x2e, 0xcc
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
hash_tree_data,
|
||||
&[
|
||||
0xfe, 0x46, 0xf7, 0x8c, 0xa1, 0xd9, 0xc8, 0xdd, 0x47, 0x9e, 0x6c, 0x32, 0x7c, 0x38,
|
||||
0x7f, 0x09, 0xe1, 0x58, 0x92, 0xa3, 0xb6, 0xbd, 0x96, 0xef, 0x10, 0xe8, 0x30, 0xb0,
|
||||
0x37, 0x8d, 0xef, 0x9a, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
|
||||
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
|
||||
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
|
||||
],
|
||||
);
|
||||
|
||||
// Updated hash tree should match newly generated tree.
|
||||
let (new_root_digest, new_hash_tree_data) =
|
||||
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
|
||||
assert_eq!(new_root_digest, root_digest);
|
||||
assert_eq!(new_hash_tree_data, hash_tree_data);
|
||||
|
||||
// Data should validate successfully.
|
||||
hash_tree
|
||||
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
|
||||
.unwrap();
|
||||
|
||||
// But not if the data is corrupted.
|
||||
input.rewind().unwrap();
|
||||
input.write_all(b"Bad").unwrap();
|
||||
|
||||
hash_tree
|
||||
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod bootimage;
|
||||
pub mod compression;
|
||||
pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod ota;
|
||||
pub mod padding;
|
||||
pub mod payload;
|
||||
|
||||
@@ -40,7 +40,7 @@ const NAME_PAYLOAD_METADATA: &str = "payload_metadata.bin";
|
||||
pub const PF_NAME: &str = "ota-property-files";
|
||||
pub const PF_STREAMING_NAME: &str = "ota-streaming-property-files";
|
||||
|
||||
const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
|
||||
pub const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
|
||||
|
||||
const COMMENT_MESSAGE: &[u8] = b"signed by avbroot\0";
|
||||
|
||||
|
||||
+311
-111
@@ -6,6 +6,7 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -13,19 +14,22 @@ use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use bzip2::write::BzDecoder;
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
write::XzEncoder,
|
||||
};
|
||||
use num_traits::ToPrimitive;
|
||||
use prost::Message;
|
||||
use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
|
||||
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
|
||||
};
|
||||
use ring::digest::{Context, Digest};
|
||||
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
use x509_cert::Certificate;
|
||||
use xz2::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
write::XzEncoder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
@@ -34,9 +38,10 @@ use crate::{
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, HashingWriter, ReadDiscardExt,
|
||||
ReadSeekReopen, Reopen, SharedCursor, WriteSeek,
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadSeekReopen, WriteSeek,
|
||||
WriteSeekReopen,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
const OTA_MAGIC: &[u8; 4] = b"CrAU";
|
||||
@@ -79,6 +84,8 @@ pub enum Error {
|
||||
size: u64,
|
||||
block_size: u32,
|
||||
},
|
||||
#[error("Destination extents are not in order")]
|
||||
ExtentsNotInOrder,
|
||||
#[error("Partition not found in payload: {0}")]
|
||||
MissingPartition(String),
|
||||
#[error("Partitions not found in payload: {0:?}")]
|
||||
@@ -92,7 +99,7 @@ pub enum Error {
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] xz2::stream::Error),
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
@@ -535,98 +542,6 @@ impl<W: Write> Write for PayloadWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer to produce a compressed partition image suitable for directly
|
||||
/// inserting into a `payload.bin`'s blob section. The data is XZ-compressed at
|
||||
/// a low compression level, primarily to collapse zeros, and the
|
||||
/// [`PartitionUpdate`] instance is updated with the final size and hash. The
|
||||
/// entire data will be represented as a single [`InstallOperation`] and
|
||||
/// [`InstallOperation::data_offset`] will be set to `None`.
|
||||
pub struct CompressedPartitionWriter<W: Write> {
|
||||
inner: XzEncoder<HashingWriter<CountingWriter<W>>>,
|
||||
block_size: u32,
|
||||
h_uncompressed: Context,
|
||||
written: u64,
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedPartitionWriter<W> {
|
||||
pub fn new(writer: W, block_size: u32) -> Result<Self> {
|
||||
let counting_writer = CountingWriter::new(writer);
|
||||
let hashing_writer =
|
||||
HashingWriter::new(counting_writer, Context::new(&ring::digest::SHA256));
|
||||
|
||||
// AOSP's payload_consumer does not support CRC during decompression.
|
||||
// Also, we intentionally pick the lowest compression level since we
|
||||
// primarily care about squishing zeros. The non-zero portions of boot
|
||||
// images are usually already-compressed kernels and ramdisks.
|
||||
let stream = Stream::new_easy_encoder(0, Check::None)?;
|
||||
let xz_writer = XzEncoder::new_stream(hashing_writer, stream);
|
||||
|
||||
Ok(Self {
|
||||
inner: xz_writer,
|
||||
block_size,
|
||||
h_uncompressed: Context::new(&ring::digest::SHA256),
|
||||
written: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Finish writing and update the [`PartitionUpdate`] instance with the new
|
||||
/// size, hash, and install operation metadata.
|
||||
pub fn finish(mut self, partition: &mut PartitionUpdate) -> Result<W> {
|
||||
if self.written % u64::from(self.block_size) != 0 {
|
||||
return Err(Error::InvalidPartitionSize {
|
||||
name: partition.partition_name.clone(),
|
||||
size: self.written,
|
||||
block_size: self.block_size,
|
||||
});
|
||||
}
|
||||
|
||||
self.inner.flush()?;
|
||||
|
||||
let hashing_writer = self.inner.finish()?;
|
||||
let digest_uncompressed = self.h_uncompressed.finish();
|
||||
let (counting_writer, context_compressed) = hashing_writer.finish();
|
||||
let digest_compressed = context_compressed.finish();
|
||||
// XzEncoder::total_out() cannot be used for an exact byte count because
|
||||
// XzEncoder::finish() writes data.
|
||||
let (writer, size_compressed) = counting_writer.finish();
|
||||
|
||||
partition.new_partition_info = Some(PartitionInfo {
|
||||
size: Some(self.written),
|
||||
hash: Some(digest_uncompressed.as_ref().to_vec()),
|
||||
});
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(0),
|
||||
num_blocks: Some(self.written / u64::from(self.block_size)),
|
||||
};
|
||||
|
||||
// data_offset must be manually updated by the caller.
|
||||
let mut operation = InstallOperation::default();
|
||||
operation.set_type(Type::ReplaceXz);
|
||||
operation.data_length = Some(size_compressed);
|
||||
operation.dst_extents.push(extent);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
partition.operations.clear();
|
||||
partition.operations.push(operation);
|
||||
|
||||
Ok(writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for CompressedPartitionWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.inner.write(buf)?;
|
||||
self.h_uncompressed.update(&buf[..n]);
|
||||
self.written += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the payload signatures using the specified certificate and check that
|
||||
/// the digests in `payload_properties.txt` are correct.
|
||||
pub fn verify_payload(
|
||||
@@ -867,29 +782,29 @@ pub fn apply_operation(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the specified image from the payload into memory. This is done
|
||||
/// multithreaded and uses rayon's global thread pool. `open_payload` will be
|
||||
/// called from multiple threads.
|
||||
pub fn extract_image_to_memory(
|
||||
/// Extract the specified image from the payload. This is done multithreaded and
|
||||
/// uses rayon's global thread pool. Both the `payload` and `output` streams
|
||||
/// will be reopened from multiple threads.
|
||||
pub fn extract_image(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
header: &PayloadHeader,
|
||||
partition_name: &str,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<SharedCursor> {
|
||||
) -> Result<()> {
|
||||
let partition = header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.find(|p| p.partition_name == partition_name)
|
||||
.ok_or_else(|| Error::MissingPartition(partition_name.to_owned()))?;
|
||||
let stream = SharedCursor::default();
|
||||
|
||||
partition
|
||||
.operations
|
||||
.par_iter()
|
||||
.map(|op| -> Result<()> {
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = stream.reopen()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
@@ -902,9 +817,7 @@ pub fn extract_image_to_memory(
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(stream)
|
||||
.collect::<Result<_>>()
|
||||
}
|
||||
|
||||
/// Extract the specified partition images from the payload into writers. This
|
||||
@@ -954,3 +867,290 @@ pub fn extract_images<'a>(
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8>, Digest)> {
|
||||
let reader = Cursor::new(raw_data);
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let hashing_writer = HashingWriter::new(writer, Context::new(&ring::digest::SHA256));
|
||||
|
||||
// AOSP's payload_consumer does not support checking CRC during
|
||||
// decompression. Also, we intentionally pick the lowest compression level
|
||||
// since we primarily care about squishing zeros. The non-zero portions of
|
||||
// boot images are usually already-compressed kernels and ramdisks.
|
||||
let stream = Stream::new_easy_encoder(0, Check::None)?;
|
||||
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
|
||||
|
||||
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)?;
|
||||
|
||||
let hashing_writer = xz_writer.finish()?;
|
||||
let (writer, context_compressed) = hashing_writer.finish();
|
||||
let digest_compressed = context_compressed.finish();
|
||||
let data = writer.into_inner();
|
||||
|
||||
Ok((data, digest_compressed))
|
||||
}
|
||||
|
||||
/// Compress the image and return the corresponding information to insert into
|
||||
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
|
||||
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
|
||||
/// then written in parallel (but in order) to the output. Each chunk will have
|
||||
/// a corresponding [`InstallOperation`] in the return value. The caller must
|
||||
/// update [`InstallOperation::data_offset`] in each operation manually because
|
||||
/// the initial values are relative to 0.
|
||||
pub fn compress_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>)> {
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
let file_size = input.reopen_boxed()?.seek(SeekFrom::End(0))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
return Err(Error::InvalidPartitionSize {
|
||||
name: partition_name.to_owned(),
|
||||
size: file_size,
|
||||
block_size,
|
||||
});
|
||||
}
|
||||
|
||||
let chunks_total = util::div_ceil(file_size, CHUNK_SIZE);
|
||||
let mut bytes_compressed = 0;
|
||||
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
|
||||
let mut operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
// without reading the entire file into memory. This is necessary because we
|
||||
// need to compute the checksum of the entire file.
|
||||
while (operations.len() as u64) < chunks_total {
|
||||
let chunks_done = operations.len() as u64;
|
||||
let chunks_group = (chunks_total - chunks_done).min(CHUNK_GROUP);
|
||||
|
||||
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<(u64, Vec<u8>)> {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
|
||||
file_size % CHUNK_SIZE
|
||||
} else {
|
||||
CHUNK_SIZE
|
||||
};
|
||||
let mut data = vec![0u8; chunk_size as usize];
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_exact(&mut data)?;
|
||||
|
||||
Ok((offset, data))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (_, data) in &uncompressed_data_group {
|
||||
context_uncompressed.update(data);
|
||||
}
|
||||
|
||||
let mut compressed_data_group = uncompressed_data_group
|
||||
.into_par_iter()
|
||||
.map(
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
num_blocks: Some(raw_data.len() as u64 / u64::from(block_size)),
|
||||
};
|
||||
|
||||
let mut operation = InstallOperation::default();
|
||||
operation.set_type(Type::ReplaceXz);
|
||||
operation.data_length = Some(data.len() as u64);
|
||||
operation.dst_extents.push(extent);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
Ok((data, operation))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (data, operation) in &mut compressed_data_group {
|
||||
operation.data_offset = Some(bytes_compressed);
|
||||
bytes_compressed += data.len() as u64;
|
||||
}
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, operation)| -> Result<InstallOperation> {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
Ok(operation)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
operations.extend(group_operations.into_iter());
|
||||
}
|
||||
|
||||
let digest_uncompressed = context_uncompressed.finish();
|
||||
let partition_info = PartitionInfo {
|
||||
size: Some(file_size),
|
||||
hash: Some(digest_uncompressed.as_ref().to_vec()),
|
||||
};
|
||||
|
||||
Ok((partition_info, operations))
|
||||
}
|
||||
|
||||
fn extents_sorted(operations: &[InstallOperation]) -> bool {
|
||||
let mut offset = 0;
|
||||
|
||||
for operation in operations {
|
||||
if operation.dst_extents.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for extent in &operation.dst_extents {
|
||||
let Some(start) = extent.start_block else {
|
||||
return false;
|
||||
};
|
||||
let Some(size) = extent.num_blocks else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if start != offset {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(next) = start.checked_add(size) else {
|
||||
return false;
|
||||
};
|
||||
offset = next;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Compress the modified image and update the specified [`PartitionInfo`] and
|
||||
/// list of [`InstallOperation`]s. [`InstallOperation`]s that do not match any
|
||||
/// byte range in `ranges` will not be compressed. The caller must update
|
||||
/// [`InstallOperation::data_offset`] in each operation manually because the
|
||||
/// initial values are relative to 0.
|
||||
///
|
||||
/// Returns the ranges of indices of `operations` that were updated.
|
||||
pub fn compress_modified_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
block_size: u32,
|
||||
partition_info: &mut PartitionInfo,
|
||||
operations: &mut [InstallOperation],
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<usize>>> {
|
||||
const OPERATION_GROUP: usize = 32;
|
||||
|
||||
// Full OTAs created by payload_generator have one extent per operation and
|
||||
// they're all in order with no gaps. Verify this so we can take advantage
|
||||
// of this layout.
|
||||
if !extents_sorted(operations) {
|
||||
return Err(Error::ExtentsNotInOrder);
|
||||
}
|
||||
|
||||
let groups_total = util::div_ceil(operations.len(), OPERATION_GROUP);
|
||||
let mut bytes_compressed = 0;
|
||||
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
|
||||
let mut modified_operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
// without reading the entire file into memory. This is necessary because we
|
||||
// need to compute the checksum of the entire file.
|
||||
for group in 0..groups_total {
|
||||
let operation_start = group * OPERATION_GROUP;
|
||||
let operation_size = (operations.len() - operation_start).min(OPERATION_GROUP);
|
||||
let operation_end = operation_start + operation_size;
|
||||
|
||||
let uncompressed_data_group = operations[operation_start..operation_end]
|
||||
.par_iter()
|
||||
.map(|operation| -> Result<(Vec<u8>, bool)> {
|
||||
let extents_start = operation.dst_extents[0]
|
||||
.start_block()
|
||||
.checked_mul(u64::from(block_size))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_start"))?;
|
||||
let extents_size = operation
|
||||
.dst_extents
|
||||
.iter()
|
||||
.map(|e| e.num_blocks())
|
||||
.try_fold(0u64, |acc, n| acc.checked_add(n))
|
||||
.and_then(|n| n.checked_mul(u64::from(block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
|
||||
let extents_end = extents_start
|
||||
.checked_add(extents_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_end"))?;
|
||||
let extents_size = extents_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(extents_start))?;
|
||||
|
||||
let mut data = vec![0u8; extents_size];
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_exact(&mut data)?;
|
||||
|
||||
let was_modified = util::ranges_overlaps(ranges, &(extents_start..extents_end));
|
||||
|
||||
Ok((data, was_modified))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (data, _) in &uncompressed_data_group {
|
||||
context_uncompressed.update(data);
|
||||
}
|
||||
|
||||
// Only compress the modified chunks.
|
||||
let mut compressed_data_group = operations[operation_start..operation_end]
|
||||
.par_iter_mut()
|
||||
.enumerate()
|
||||
.zip(uncompressed_data_group)
|
||||
.filter(|(_, (_, was_modified))| *was_modified)
|
||||
.map(
|
||||
|((i_rel, operation), (raw_data, _))| -> Result<(Vec<u8>, usize, &mut InstallOperation)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
|
||||
operation.set_type(Type::ReplaceXz);
|
||||
operation.data_length = Some(data.len() as u64);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
Ok((data, i_rel + operation_start, operation))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (data, _, operation) in &mut compressed_data_group {
|
||||
operation.data_offset = Some(bytes_compressed);
|
||||
bytes_compressed += data.len() as u64;
|
||||
}
|
||||
|
||||
let modified_group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, i, operation)| {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
Ok(i..i + 1)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
modified_operations.extend(modified_group_operations);
|
||||
}
|
||||
|
||||
let digest_uncompressed = context_uncompressed.finish();
|
||||
partition_info.hash = Some(digest_uncompressed.as_ref().to_vec());
|
||||
|
||||
Ok(util::merge_overlapping(&modified_operations))
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,12 +13,12 @@
|
||||
// We use pb-rs' nostd mode. See build.rs.
|
||||
extern crate alloc;
|
||||
|
||||
pub mod boot;
|
||||
pub mod cli;
|
||||
pub mod crypto;
|
||||
pub mod escape;
|
||||
pub mod format;
|
||||
pub mod octal;
|
||||
pub mod patch;
|
||||
pub mod protobuf;
|
||||
pub mod stream;
|
||||
pub mod util;
|
||||
|
||||
+15
-3
@@ -8,9 +8,11 @@ use std::sync::{
|
||||
Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::error;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn main() {
|
||||
// Set up a cancel signal so we can properly clean up any temporary files.
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
@@ -22,5 +24,15 @@ fn main() -> Result<()> {
|
||||
.expect("Failed to set signal handler");
|
||||
}
|
||||
|
||||
avbroot::cli::args::main(&cancel_signal)
|
||||
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
|
||||
error!("{e:?}");
|
||||
} else {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,46 +5,50 @@
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Cursor, Read, Seek, Write},
|
||||
io::{self, BufRead, BufReader, Cursor, Read, Seek},
|
||||
num::ParseIntError,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
slice,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPrivateKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, warn, Span};
|
||||
use x509_cert::Certificate;
|
||||
use xz2::{
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
use zip::{result::ZipError, ZipArchive};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
format::{
|
||||
avb::{self, Descriptor},
|
||||
avb::{self, AppendedDescriptorMut, Footer, Header},
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntry, CpioEntryData},
|
||||
},
|
||||
stream::{self, FromReader, HashingWriter, SectionReader, ToWriter},
|
||||
patch::otacert::{self, OtaCertBuildFlags},
|
||||
stream::{self, FromReader, HashingWriter, ReadSeek, SectionReader, ToWriter, WriteSeek},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Boot image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash descriptor found in vbmeta header")]
|
||||
NoHashDescriptor,
|
||||
#[error("Found multiple hash descriptors in vbmeta header")]
|
||||
MultipleHashDescriptors,
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
#[error("Failed to parse Magisk version from line: {0:?}")]
|
||||
@@ -61,8 +65,10 @@ pub enum Error {
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("CPIO error")]
|
||||
Cpio(#[from] cpio::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] xz2::stream::Error),
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
@@ -81,6 +87,12 @@ fn load_ramdisk(
|
||||
let mut reader = CompressedReader::new(raw_reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal)?;
|
||||
|
||||
trace!(
|
||||
"Loaded {:?} ramdisk with {} entries",
|
||||
reader.format(),
|
||||
entries.len(),
|
||||
);
|
||||
|
||||
Ok((entries, reader.format()))
|
||||
}
|
||||
|
||||
@@ -93,11 +105,31 @@ fn save_ramdisk(
|
||||
let mut writer = CompressedWriter::new(raw_writer, format)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal)?;
|
||||
|
||||
trace!("Wrote {format:?} ramdisk with {} entries", entries.len());
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
pub trait BootImagePatcher {
|
||||
pub struct BootImageInfo {
|
||||
pub header: Header,
|
||||
pub footer: Footer,
|
||||
pub image_size: u64,
|
||||
pub boot_image: BootImage,
|
||||
}
|
||||
|
||||
pub trait BootImagePatch {
|
||||
fn patcher_name(&self) -> &'static str;
|
||||
|
||||
/// Inspect a set of possible candidate boot images and return a list of
|
||||
/// image names that can be patched. Both the boot image and the AVB info
|
||||
/// can be inspected, but during patching, only the boot image is available.
|
||||
fn find_targets<'a>(
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>>;
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -117,20 +149,24 @@ impl MagiskRootPatcher {
|
||||
// RULESDEVICE config option, which stored the writable block device as an
|
||||
// rdev major/minor pair, which was not consistent across reboots and was
|
||||
// replaced by PREINITDEVICE
|
||||
const VERS_SUPPORTED: &[Range<u32>] = &[25102..25207, 25211..26500];
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..27100];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
const VER_PATCH_VBMETA: Range<u32> = Self::VERS_SUPPORTED[0].start..26202;
|
||||
const VER_XZ_BACKUP: Range<u32> =
|
||||
26403..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
|
||||
pub fn new(
|
||||
path: &Path,
|
||||
preinit_device: Option<&str>,
|
||||
random_seed: Option<u64>,
|
||||
ignore_compatibility: bool,
|
||||
warning_fn: impl Fn(&str) + Send + 'static,
|
||||
) -> Result<Self> {
|
||||
let version = Self::get_version(path)?;
|
||||
|
||||
debug!("Found Magisk version: {version}");
|
||||
|
||||
if !Self::VERS_SUPPORTED.iter().any(|v| v.contains(&version)) {
|
||||
let msg = format!(
|
||||
"Unsupported Magisk version {} (supported: {:?})",
|
||||
@@ -139,7 +175,7 @@ impl MagiskRootPatcher {
|
||||
);
|
||||
|
||||
if ignore_compatibility {
|
||||
warning_fn(&msg);
|
||||
warn!("{msg}");
|
||||
} else {
|
||||
return Err(Error::Validation(msg));
|
||||
}
|
||||
@@ -153,7 +189,7 @@ impl MagiskRootPatcher {
|
||||
);
|
||||
|
||||
if ignore_compatibility {
|
||||
warning_fn(&msg);
|
||||
warn!("{msg}");
|
||||
} else {
|
||||
return Err(Error::Validation(msg));
|
||||
}
|
||||
@@ -185,6 +221,8 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
|
||||
if let Some(suffix) = line.trim_end().strip_prefix("MAGISK_VER_CODE=") {
|
||||
trace!("Magisk version code line: {line:?}");
|
||||
|
||||
let version = suffix
|
||||
.parse()
|
||||
.map_err(|e| Error::ParseMagiskVersion(suffix.to_owned(), e))?;
|
||||
@@ -193,6 +231,18 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn xz_compress(reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32)?;
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = XzEncoder::new_stream(raw_writer, stream);
|
||||
|
||||
stream::copy(reader, &mut writer, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
/// Compare old and new ramdisk entry lists, creating the Magisk `.backup/`
|
||||
/// directory structure. `.backup/.rmlist` will contain a sorted list of
|
||||
/// NULL-terminated strings, listing which files were newly added or
|
||||
@@ -200,7 +250,12 @@ impl MagiskRootPatcher {
|
||||
/// entries as `.backup/<path>`.
|
||||
///
|
||||
/// Both lists and entries within the lists may be mutated.
|
||||
fn apply_magisk_backup(old_entries: &mut [CpioEntry], new_entries: &mut Vec<CpioEntry>) {
|
||||
fn apply_magisk_backup(
|
||||
old_entries: &mut [CpioEntry],
|
||||
new_entries: &mut Vec<CpioEntry>,
|
||||
xz_compress: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
cpio::sort(old_entries);
|
||||
cpio::sort(new_entries);
|
||||
|
||||
@@ -245,11 +300,54 @@ impl MagiskRootPatcher {
|
||||
|
||||
new_entries.push(CpioEntry::new_directory(b".backup", 0));
|
||||
|
||||
debug!(
|
||||
"Removed entries: {:?}",
|
||||
rm_list
|
||||
.split(|b| *b == 0)
|
||||
.filter(|e| !e.is_empty())
|
||||
.map(|p| p.as_bstr().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
debug!(
|
||||
"Added/changed entries: {:?}",
|
||||
to_back_up
|
||||
.iter()
|
||||
.map(|e| e.path.as_bstr().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
for old_entry in to_back_up {
|
||||
let mut new_entry = old_entry.clone();
|
||||
new_entry.path = b".backup/".to_vec();
|
||||
new_entry.path.extend(&old_entry.path);
|
||||
new_entries.push(new_entry);
|
||||
let mut new_path = b".backup/".to_vec();
|
||||
new_path.extend(&old_entry.path);
|
||||
|
||||
let mut new_data = None;
|
||||
|
||||
if xz_compress {
|
||||
if let CpioEntryData::Data(data) = &old_entry.data {
|
||||
new_path.extend(b".xz");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
new_data = Some(CpioEntryData::Data(buf));
|
||||
}
|
||||
}
|
||||
|
||||
new_entries.push(CpioEntry {
|
||||
path: new_path,
|
||||
data: new_data.unwrap_or_else(|| old_entry.data.clone()),
|
||||
inode: old_entry.inode,
|
||||
file_type: old_entry.file_type,
|
||||
file_mode: old_entry.file_mode,
|
||||
uid: old_entry.uid,
|
||||
gid: old_entry.gid,
|
||||
nlink: old_entry.nlink,
|
||||
mtime: old_entry.mtime,
|
||||
dev_maj: old_entry.dev_maj,
|
||||
dev_min: old_entry.dev_min,
|
||||
rdev_maj: old_entry.rdev_maj,
|
||||
rdev_min: old_entry.rdev_min,
|
||||
crc32: old_entry.crc32,
|
||||
});
|
||||
}
|
||||
|
||||
new_entries.push(CpioEntry::new_file(
|
||||
@@ -257,10 +355,32 @@ impl MagiskRootPatcher {
|
||||
0,
|
||||
CpioEntryData::Data(rm_list),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for MagiskRootPatcher {
|
||||
impl BootImagePatch for MagiskRootPatcher {
|
||||
fn patcher_name(&self) -> &'static str {
|
||||
"MagiskRootPatcher"
|
||||
}
|
||||
|
||||
fn find_targets<'a>(
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
if boot_images.contains_key("init_boot") {
|
||||
targets.push("init_boot");
|
||||
} else if boot_images.contains_key("boot") {
|
||||
targets.push("boot");
|
||||
};
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let zip_reader =
|
||||
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
|
||||
@@ -304,7 +424,9 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
));
|
||||
}
|
||||
|
||||
// Add xz-compressed magisk32 and magisk64.
|
||||
// Add xz-compressed magisk32 and magisk64. We currently unconditionally
|
||||
// include magisk32 because the boot image itself doesn't contain
|
||||
// sufficient information to determine if a device is 64-bit only.
|
||||
let mut xz_files = HashMap::<&str, &[u8]>::new();
|
||||
xz_files.insert(
|
||||
"lib/armeabi-v7a/libmagisk32.so",
|
||||
@@ -318,34 +440,34 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
// Add stub apk, which only exists after Magisk commit
|
||||
// ad0e6511e11ebec65aa9b5b916e1397342850319.
|
||||
if zip.file_names().any(|n| n == "assets/stub.apk") {
|
||||
debug!("Magisk stub found");
|
||||
xz_files.insert("assets/stub.apk", b"overlay.d/sbin/stub.xz");
|
||||
}
|
||||
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip.by_name(source)?;
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32)?;
|
||||
let mut writer = XzEncoder::new_stream(raw_writer, stream);
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
|
||||
stream::copy(reader, &mut writer, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
target,
|
||||
0o644,
|
||||
CpioEntryData::Data(raw_writer.into_inner()),
|
||||
));
|
||||
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
|
||||
}
|
||||
|
||||
// Create Magisk .backup directory structure.
|
||||
Self::apply_magisk_backup(&mut old_entries, &mut entries);
|
||||
Self::apply_magisk_backup(
|
||||
&mut old_entries,
|
||||
&mut entries,
|
||||
Self::VER_XZ_BACKUP.contains(&self.version),
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
// Create Magisk config.
|
||||
let mut magisk_config = String::new();
|
||||
magisk_config.push_str("KEEPVERITY=true\n");
|
||||
magisk_config.push_str("KEEPFORCEENCRYPT=true\n");
|
||||
magisk_config.push_str("PATCHVBMETAFLAG=false\n");
|
||||
|
||||
if Self::VER_PATCH_VBMETA.contains(&self.version) {
|
||||
magisk_config.push_str("PATCHVBMETAFLAG=false\n");
|
||||
}
|
||||
|
||||
magisk_config.push_str("RECOVERYMODE=false\n");
|
||||
|
||||
if Self::VER_PREINIT_DEVICE.contains(&self.version) {
|
||||
@@ -365,6 +487,8 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
magisk_config.push_str(&format!("RANDOMSEED={:#x}\n", self.random_seed));
|
||||
}
|
||||
|
||||
trace!("Magisk config: {magisk_config:?}");
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
b".backup/.magisk",
|
||||
0,
|
||||
@@ -407,7 +531,7 @@ pub struct OtaCertPatcher {
|
||||
}
|
||||
|
||||
impl OtaCertPatcher {
|
||||
const OTACERTS_PATH: &[u8] = b"system/etc/security/otacerts.zip";
|
||||
const OTACERTS_PATH: &'static [u8] = b"system/etc/security/otacerts.zip";
|
||||
|
||||
pub fn new(cert: Certificate) -> Self {
|
||||
Self { cert }
|
||||
@@ -428,6 +552,10 @@ impl OtaCertPatcher {
|
||||
let mut certificates = vec![];
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
if ramdisk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let Some(entry) = entries.iter().find(|e| e.path == Self::OTACERTS_PATH) else {
|
||||
continue;
|
||||
@@ -441,6 +569,7 @@ impl OtaCertPatcher {
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip.by_index(index)?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {}", zip_entry.name());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -452,62 +581,89 @@ impl OtaCertPatcher {
|
||||
Ok(certificates)
|
||||
}
|
||||
|
||||
fn patch_ramdisk(&self, data: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(data, cancel_signal)?;
|
||||
fn patch_ramdisk(
|
||||
&self,
|
||||
ramdisk: &mut Vec<u8>,
|
||||
zip: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let Some(entry) = entries.iter_mut().find(|e| e.path == Self::OTACERTS_PATH) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
// Create a new otacerts archive. The old certs are ignored since
|
||||
// flashing a stock OTA will render the device unbootable.
|
||||
{
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &self.cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
entry.data = CpioEntryData::Data(raw_writer.into_inner());
|
||||
}
|
||||
entry.data = CpioEntryData::Data(zip.to_vec());
|
||||
|
||||
// Repack ramdisk.
|
||||
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
*ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for OtaCertPatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let patched_any = match boot_image {
|
||||
BootImage::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
|
||||
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
let mut patched = false;
|
||||
impl BootImagePatch for OtaCertPatcher {
|
||||
fn patcher_name(&self) -> &'static str {
|
||||
"OtaCertPatcher"
|
||||
}
|
||||
|
||||
for ramdisk in &mut b.ramdisks {
|
||||
if self.patch_ramdisk(ramdisk, cancel_signal)? {
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
fn find_targets<'a>(
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
'outer: for (name, info) in boot_images {
|
||||
let ramdisks = match &info.boot_image {
|
||||
BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk),
|
||||
BootImage::VendorV3Through4(b) => &b.ramdisks,
|
||||
};
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
if ramdisk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
patched
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
if entries.iter().any(|e| e.path == Self::OTACERTS_PATH) {
|
||||
targets.push(*name);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let ramdisks = match boot_image {
|
||||
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
|
||||
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
|
||||
BootImage::VendorV3Through4(b) => &mut b.ramdisks,
|
||||
};
|
||||
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())?;
|
||||
trace!("Generated new {} byte otacerts.zip", new_zip.len());
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
if ramdisk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Fail hard if otacerts does not exist. We don't want to lock the user
|
||||
// out of future updates if the OTA certificate mechanism has changed.
|
||||
if !patched_any {
|
||||
return Err(Error::Validation(format!(
|
||||
"No ramdisk contains {:?}",
|
||||
Self::OTACERTS_PATH.as_bstr(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Err(Error::Validation(format!(
|
||||
"No ramdisk contains {:?}",
|
||||
Self::OTACERTS_PATH.as_bstr(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +676,6 @@ impl BootImagePatcher for OtaCertPatcher {
|
||||
pub struct PrepatchedImagePatcher {
|
||||
prepatched: PathBuf,
|
||||
fatal_level: u8,
|
||||
warning_fn: Box<dyn Fn(&str) + Send>,
|
||||
}
|
||||
|
||||
impl PrepatchedImagePatcher {
|
||||
@@ -528,20 +683,24 @@ impl PrepatchedImagePatcher {
|
||||
const MAX_LEVEL: u8 = 2;
|
||||
|
||||
// We compile without Unicode support so we have to use [0-9] instead of \d.
|
||||
const VERSION_REGEX: &str = r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
|
||||
const VERSION_REGEX: &'static str =
|
||||
r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
|
||||
|
||||
pub fn new(
|
||||
prepatched: &Path,
|
||||
fatal_level: u8,
|
||||
warning_fn: impl Fn(&str) + Send + 'static,
|
||||
) -> Self {
|
||||
pub fn new(prepatched: &Path, fatal_level: u8) -> Self {
|
||||
Self {
|
||||
prepatched: prepatched.to_owned(),
|
||||
fatal_level,
|
||||
warning_fn: Box::new(warning_fn),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_prepatched_image(&self) -> Result<BootImage> {
|
||||
let raw_reader =
|
||||
File::open(&self.prepatched).map_err(|e| Error::File(self.prepatched.clone(), e))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))?;
|
||||
|
||||
Ok(boot_image)
|
||||
}
|
||||
|
||||
fn get_kmi_version(kernel: &[u8]) -> Result<Option<String>> {
|
||||
let mut decompressed = vec![];
|
||||
{
|
||||
@@ -570,14 +729,38 @@ impl PrepatchedImagePatcher {
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for PrepatchedImagePatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let prepatched_image = {
|
||||
let raw_reader = File::open(&self.prepatched)
|
||||
.map_err(|e| Error::File(self.prepatched.clone(), e))?;
|
||||
BootImage::from_reader(BufReader::new(raw_reader))?
|
||||
impl BootImagePatch for PrepatchedImagePatcher {
|
||||
fn patcher_name(&self) -> &'static str {
|
||||
"PrepatchedImagePatcher"
|
||||
}
|
||||
|
||||
fn find_targets<'a>(
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
let prepatched_image = self.load_prepatched_image()?;
|
||||
|
||||
let has_kernel = match prepatched_image {
|
||||
BootImage::V0Through2(b) => !b.kernel.is_empty(),
|
||||
BootImage::V3Through4(b) => !b.kernel.is_empty(),
|
||||
BootImage::VendorV3Through4(_) => false,
|
||||
};
|
||||
|
||||
let mut targets = vec![];
|
||||
|
||||
if !has_kernel && boot_images.contains_key("init_boot") {
|
||||
targets.push("init_boot");
|
||||
} else if boot_images.contains_key("boot") {
|
||||
targets.push("boot");
|
||||
};
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let prepatched_image = self.load_prepatched_image()?;
|
||||
|
||||
// Level 0: Warnings that don't affect booting
|
||||
// Level 1: Warnings that may affect booting
|
||||
// Level 2: Warnings that are very likely to affect booting
|
||||
@@ -723,7 +906,7 @@ impl BootImagePatcher for PrepatchedImagePatcher {
|
||||
msg.push_str(warning);
|
||||
}
|
||||
|
||||
(self.warning_fn)(&msg);
|
||||
warn!("{msg}");
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
@@ -743,60 +926,138 @@ impl BootImagePatcher for PrepatchedImagePatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run each patcher against the boot image with the vbmeta footer stripped off
|
||||
/// and then re-sign the image.
|
||||
pub fn patch_boot(
|
||||
mut reader: impl Read + Seek,
|
||||
writer: impl Write + Seek,
|
||||
key: &RsaPrivateKey,
|
||||
patchers: &[Box<dyn BootImagePatcher + Send>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
pub fn load_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
) -> Result<HashMap<&'a str, BootImageInfo>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
|
||||
let mut boot_image = BootImage::from_reader(section_reader)?;
|
||||
names
|
||||
.par_iter()
|
||||
.map(|name| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut reader = open_input(name)?;
|
||||
|
||||
for patcher in patchers {
|
||||
patcher.patch(&mut boot_image, cancel_signal)?;
|
||||
}
|
||||
let (header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
|
||||
let mut descriptor_iter = header.descriptors.iter_mut().filter_map(|d| {
|
||||
if let Descriptor::Hash(h) = d {
|
||||
Some(h)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
|
||||
let boot_image = BootImage::from_reader(section_reader)?;
|
||||
|
||||
let Some(descriptor) = descriptor_iter.next() else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
trace!("Loaded {image_size} byte boot image: {name}");
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
descriptor.hash_algorithm = "sha256".to_owned();
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if descriptor_iter.next().is_some() {
|
||||
return Err(Error::MultipleHashDescriptors);
|
||||
}
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
|
||||
|
||||
Ok(())
|
||||
Ok((*name, info))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Apply applicable patches to the list of specified boot images. For each
|
||||
/// image, the applicable patchers run in the same order as in `patchers`. All
|
||||
/// operations run in parallel where possible. Only the patcher execution for a
|
||||
/// given image is guaranteed to be sequential. The input and output files will
|
||||
/// be opened from multiple threads, but at most once each.
|
||||
pub fn patch_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
key: &RsaPrivateKey,
|
||||
patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<HashSet<&'a str>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
// Preparse all images. Some patchers need to inspect every candidate.
|
||||
let mut images = load_boot_images(names, open_input)?;
|
||||
|
||||
// Find the targets that each patcher wants to patch.
|
||||
let all_targets = patchers
|
||||
.par_iter()
|
||||
.map(|p| {
|
||||
let _span =
|
||||
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
|
||||
p.find_targets(&images, cancel_signal).and_then(|targets| {
|
||||
if targets.is_empty() {
|
||||
Err(Error::NoTargets(p.patcher_name()))
|
||||
} else {
|
||||
debug!("Found patcher targets: {targets:?}");
|
||||
Ok(targets)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
debug!("All patcher targets: {all_targets:?}");
|
||||
|
||||
// Regroup data so we can parallelize by target.
|
||||
let mut groups = HashMap::<&str, (BootImageInfo, Vec<&Box<dyn BootImagePatch + Sync>>)>::new();
|
||||
for (patcher, targets) in patchers.iter().zip(all_targets.into_iter()) {
|
||||
for target in targets {
|
||||
groups
|
||||
.entry(target)
|
||||
.or_insert_with(|| (images.remove(target).unwrap(), vec![]))
|
||||
.1
|
||||
.push(patcher);
|
||||
}
|
||||
}
|
||||
|
||||
// Deallocate all untouched images.
|
||||
drop(images);
|
||||
|
||||
// Apply all patches.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.try_for_each(|(_, (info, patchers))| -> Result<()> {
|
||||
patchers.iter().try_for_each(|p| {
|
||||
let _span =
|
||||
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
|
||||
p.patch(&mut info.boot_image, cancel_signal)
|
||||
})
|
||||
})?;
|
||||
|
||||
// Resign and write new images.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.map(|(name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info.header.appended_descriptor_mut()?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
|
||||
let writer = open_output(name)?;
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
descriptor.hash_algorithm = "sha256".to_owned();
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if !info.header.public_key.is_empty() {
|
||||
debug!("Signing boot image");
|
||||
info.header.set_algo_for_key(key)?;
|
||||
info.header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(writer, &info.header, &mut info.footer, info.image_size)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(groups.keys().cloned().collect())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
pub mod boot;
|
||||
pub mod otacert;
|
||||
pub mod system;
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use x509_cert::{der::asn1::BitString, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
|
||||
|
||||
use crate::{crypto, format::ota};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("New otacerts.zip is too small to pad to {0} bytes")]
|
||||
ZipTooSmall(usize),
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Pad a non-zip64 zip file to the specified size by adding null bytes to the
|
||||
/// archive comment field.
|
||||
pub fn pad_zip(data: &mut Vec<u8>, size: usize) -> Result<()> {
|
||||
match size.cmp(&data.len()) {
|
||||
Ordering::Equal => Ok(()),
|
||||
Ordering::Less => Err(Error::ZipTooLarge(size)),
|
||||
Ordering::Greater => {
|
||||
let padding = size - data.len();
|
||||
|
||||
if data.len() < 22
|
||||
|| &data[data.len() - 22..][..4] != ota::ZIP_EOCD_MAGIC
|
||||
|| padding > usize::from(u16::MAX)
|
||||
{
|
||||
return Err(Error::ZipTooSmall(size));
|
||||
}
|
||||
|
||||
// Rewrite the comment size and pad with null bytes.
|
||||
data.pop();
|
||||
data.pop();
|
||||
data.extend((padding as u16).to_le_bytes());
|
||||
data.resize(size, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Android uses X.509 as nothing more than a file format to transport RSA
|
||||
/// public keys. This is true for both the framework's RecoverySystem and
|
||||
/// recovery's otautil/verifier.cpp. The only fields that must exist are
|
||||
/// the public key and the signature algorithm. The rest can be removed with
|
||||
/// no side effects whatsoever.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct OtaCertBuildFlags: u8 {
|
||||
const COMPRESS_DEFLATE = 1 << 0;
|
||||
const REMOVE_SIGNATURE = 1 << 1;
|
||||
const REMOVE_EXTENSIONS = 1 << 2;
|
||||
const REMOVE_ISSUER = 1 << 3;
|
||||
const REMOVE_SUBJECT = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an `otacerts.zip` file containing the specified certificate.
|
||||
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
|
||||
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Stored
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
} else {
|
||||
let mut modified = cert.clone();
|
||||
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
|
||||
modified.signature = BitString::from_bytes(&[])?;
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
|
||||
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
|
||||
extensions.clear();
|
||||
}
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
|
||||
modified.tbs_certificate.issuer.0.clear();
|
||||
modified.tbs_certificate.issuer_unique_id = None;
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SUBJECT) {
|
||||
modified.tbs_certificate.subject.0.clear();
|
||||
modified.tbs_certificate.subject_unique_id = None;
|
||||
}
|
||||
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
/// Create an `otacerts.zip` file padded to the specified size.
|
||||
///
|
||||
/// This will incrementally remove unneeded components from the certificate to
|
||||
/// meet the size limit if needed.
|
||||
pub fn create_zip_with_size(cert: &Certificate, size: usize) -> Result<Vec<u8>> {
|
||||
let mut flags = OtaCertBuildFlags::empty();
|
||||
|
||||
for additional_flag in [
|
||||
OtaCertBuildFlags::empty(),
|
||||
OtaCertBuildFlags::COMPRESS_DEFLATE,
|
||||
OtaCertBuildFlags::REMOVE_SIGNATURE,
|
||||
OtaCertBuildFlags::REMOVE_EXTENSIONS,
|
||||
OtaCertBuildFlags::REMOVE_ISSUER,
|
||||
OtaCertBuildFlags::REMOVE_SUBJECT,
|
||||
] {
|
||||
flags |= additional_flag;
|
||||
|
||||
trace!("Attempting to create {size} byte otacerts.zip: {flags:?}");
|
||||
|
||||
let mut data = create_zip(cert, flags)?;
|
||||
if data.len() <= size {
|
||||
trace!("Padding {} byte otacerts.zip to {size}", data.len());
|
||||
|
||||
pad_zip(&mut data, size)?;
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::ZipTooLarge(size))
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, SeekFrom},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use rsa::RsaPrivateKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, Span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer},
|
||||
ota,
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Old otacerts.zip not found in image")]
|
||||
OldZipNotFound,
|
||||
#[error("Image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash tree descriptor found in vbmeta header")]
|
||||
NoHashTreeDescriptor,
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Find the bounds of a non-zip64 zip starting from the EOCD magic offset.
|
||||
fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
|
||||
let eocd = &data[eocd_offset..];
|
||||
if eocd.len() < 22 {
|
||||
trace!("Buffer is too small to contain EOCD");
|
||||
return None;
|
||||
}
|
||||
|
||||
let cd_size = u32::from_le_bytes(eocd[12..16].try_into().unwrap()) as usize;
|
||||
let cd_offset = u32::from_le_bytes(eocd[16..20].try_into().unwrap()) as usize;
|
||||
let comment_size = usize::from(u16::from_le_bytes(eocd[20..22].try_into().unwrap()));
|
||||
|
||||
let start = eocd_offset.checked_sub(cd_size)?.checked_sub(cd_offset)?;
|
||||
let end = eocd_offset.checked_add(22)?.checked_add(comment_size)?;
|
||||
if end > data.len() {
|
||||
trace!("End of zip is out of bounds");
|
||||
return None;
|
||||
}
|
||||
|
||||
trace!("Found zip bounds: {:?}", start..end);
|
||||
|
||||
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
|
||||
let mut zip_reader = ZipArchive::new(reader).ok()?;
|
||||
|
||||
if zip_reader.is_empty() {
|
||||
// otacerts.zip files contain at least one cert.
|
||||
trace!("Zip is empty");
|
||||
return None;
|
||||
}
|
||||
|
||||
for index in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index_raw(index).ok()?;
|
||||
|
||||
if !entry.name().ends_with(".x509.pem") {
|
||||
// otacerts.zip files only contain files named this way.
|
||||
trace!("Excluded due to invalid name: {:?}", entry.name());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Found otacerts.zip candidate");
|
||||
|
||||
// There's one or more entries and every one is named *.x509.pem.
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
/// Replace `otacerts.zip` with a new one containing the new certificate, but
|
||||
/// padded to the same size. If the new zip is too large, the certificate will
|
||||
/// be modified to remove unnecessary components until it fits. All operations
|
||||
/// run in parallel where possible. The input and output must refer to the same
|
||||
/// file and will be reopened from multiple threads.
|
||||
///
|
||||
/// Returns two sorted and non-overlapping lists of byte ranges that were
|
||||
/// modified. The first list are the byte regions within the filesystem data
|
||||
/// that contained otacerts.zip. The second list is the list of byte regions
|
||||
/// outside of the filesyste, like the hash tree, FEC data, and AVB metadata.
|
||||
///
|
||||
/// If [`Error::OldZipNotFound`] is returned, the output will not have been
|
||||
/// modified.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn patch_system_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
certificate: &Certificate,
|
||||
key: &RsaPrivateKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
|
||||
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
|
||||
// This ensures that the block containing otacerts.zip's data won't cross
|
||||
// chunk boundaries.
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
let num_chunks = util::div_ceil(footer.original_image_size, CHUNK_SIZE);
|
||||
trace!("Parallel heuristics search for otacerts.zip with {num_chunks} chunks");
|
||||
|
||||
let modified_ranges = (0..num_chunks)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<Vec<Range<u64>>> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let offset = chunk * CHUNK_SIZE;
|
||||
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut ranges = Vec::<Range<u64>>::new();
|
||||
|
||||
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
|
||||
let _span = debug_span!(parent: &parent_span, "otacerts", offset, eocd_offset_rel)
|
||||
.entered();
|
||||
|
||||
let Some(bounds_rel) = find_zip_bounds(&buf, eocd_offset_rel) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let zip_size = bounds_rel.end - bounds_rel.start;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
|
||||
|
||||
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(bounds.start))?;
|
||||
writer.write_all(&new_zip)?;
|
||||
|
||||
ranges.push(bounds);
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
})
|
||||
.try_reduce(Vec::new, |mut result, item| {
|
||||
result.extend(item);
|
||||
Ok(result)
|
||||
})?;
|
||||
|
||||
if modified_ranges.is_empty() {
|
||||
return Err(Error::OldZipNotFound);
|
||||
}
|
||||
|
||||
let update_ranges = if descriptor.hash_algorithm == "sha1" {
|
||||
// Promote to a secure algorithm. SHA1 is allowed for verification only.
|
||||
// The entire hash tree and FEC data will need to be recomputed.
|
||||
let new_algorithm = "sha256".to_owned();
|
||||
|
||||
debug!(
|
||||
"Changing insecure hash algorithm {} to {new_algorithm}",
|
||||
descriptor.hash_algorithm,
|
||||
);
|
||||
|
||||
descriptor.hash_algorithm = new_algorithm;
|
||||
None
|
||||
} else {
|
||||
// Only need to update the hash tree and FEC data corresponding to the
|
||||
// modified regions.
|
||||
Some(modified_ranges.as_slice())
|
||||
};
|
||||
|
||||
descriptor.update(input, output, update_ranges, cancel_signal)?;
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
debug!("Signing system image");
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed()?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
// The hash tree, FEC data, and AVB regions will have been modified.
|
||||
let hash_tree_end = descriptor
|
||||
.tree_offset
|
||||
.checked_add(descriptor.tree_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
|
||||
let fec_data_end = descriptor
|
||||
.fec_offset
|
||||
.checked_add(descriptor.fec_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
|
||||
let header_end = footer
|
||||
.vbmeta_offset
|
||||
.checked_add(footer.vbmeta_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
|
||||
let footer_start = image_size - Footer::SIZE as u64;
|
||||
|
||||
let other_ranges = util::merge_overlapping(&[
|
||||
descriptor.tree_offset..hash_tree_end,
|
||||
descriptor.fec_offset..fec_data_end,
|
||||
footer.vbmeta_offset..header_end,
|
||||
footer_start..image_size,
|
||||
]);
|
||||
|
||||
Ok((modified_ranges, other_ranges))
|
||||
}
|
||||
+104
-1
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{fmt, path::Path};
|
||||
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
|
||||
|
||||
use num_traits::PrimInt;
|
||||
|
||||
@@ -48,3 +48,106 @@ pub fn parent_path(path: &Path) -> &Path {
|
||||
|
||||
Path::new(".")
|
||||
}
|
||||
|
||||
/// Since Rust's built-in .div_ceil() is still nightly-only.
|
||||
pub fn div_ceil<T: PrimInt>(dividend: T, divisor: T) -> T {
|
||||
dividend / divisor
|
||||
+ if dividend % divisor != T::zero() {
|
||||
T::one()
|
||||
} else {
|
||||
T::zero()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort and merge overlapping intervals.
|
||||
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
|
||||
where
|
||||
T: Ord + Clone + Copy,
|
||||
{
|
||||
let mut sections = sections.to_vec();
|
||||
sections.sort_by_key(|r| (r.start, r.end));
|
||||
|
||||
let mut result = Vec::<Range<T>>::new();
|
||||
|
||||
for section in sections {
|
||||
if section.start >= section.end {
|
||||
continue;
|
||||
} else if let Some(last) = result.last_mut() {
|
||||
if section.start <= last.end {
|
||||
last.end = last.end.max(section.end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(section);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Binary search to determine if the needle overlaps any of the ranges.
|
||||
pub fn ranges_overlaps<T>(ranges: &[Range<T>], needle: &Range<T>) -> bool
|
||||
where
|
||||
T: Ord,
|
||||
{
|
||||
if needle.start < needle.end {
|
||||
ranges
|
||||
.binary_search_by(|range| {
|
||||
if range.start > needle.end {
|
||||
Ordering::Greater
|
||||
} else if range.end <= needle.start {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary search to determine if any of the ranges contain the needle.
|
||||
pub fn ranges_contains<T>(ranges: &[Range<T>], needle: &T) -> bool
|
||||
where
|
||||
T: Ord,
|
||||
{
|
||||
ranges
|
||||
.binary_search_by(|range| {
|
||||
if range.start > *needle {
|
||||
Ordering::Greater
|
||||
} else if range.end <= *needle {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ranges_overlaps() {
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_contains() {
|
||||
assert_eq!(ranges_contains(&[0..4], &0), true);
|
||||
assert_eq!(ranges_contains(&[0..4], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ fn round_trip_appended_hash_tree_image() {
|
||||
d.fec_offset = 0;
|
||||
d.fec_size = 0;
|
||||
|
||||
d.update(&writer, &writer, &cancel_signal).unwrap();
|
||||
d.update(&writer, &writer, None, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,30 @@ vulnerability = "deny"
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
notice = "deny"
|
||||
ignore = [
|
||||
# https://rustsec.org/advisories/RUSTSEC-2023-0071
|
||||
#
|
||||
# This is a side-channel vulnerability where secrets can be leaked to an
|
||||
# attacker that is able to measure the timing of a large number of RSA
|
||||
# operations. As of 2023-12-03, there is no released version of the rsa
|
||||
# crate that contains a fix.
|
||||
#
|
||||
# For avbroot specifically, this vulnerability is not too critical for a
|
||||
# couple reasons:
|
||||
#
|
||||
# 1. avbroot performs RSA signing only at the end of lengthy processes
|
||||
# that involve a lot of disk I/O. It's very expensive to run avbroot
|
||||
# the millions of times needed to capture a sufficient amount of timing
|
||||
# data.
|
||||
# 2. During a single run of avbroot, it will only perform RSA signing a
|
||||
# handful of times. To get sufficient measurements, the attacker would
|
||||
# need to rerun avbroot. If they are able to rerun avbroot, then they
|
||||
# are also able to just read and steal the private key directly.
|
||||
#
|
||||
# avbroot has no network capabilities, so this is not inherently remotely
|
||||
# exploitable.
|
||||
"RUSTSEC-2023-0071",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
include-dev = true
|
||||
@@ -41,13 +65,14 @@ include-workspace = true
|
||||
bypass = [
|
||||
# Copies of unmodified crashwrangler objects for old macOS versions.
|
||||
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
|
||||
# Test files for liblzma's test suite
|
||||
{ name = "liblzma-sys", allow-globs = ["xz/tests/compress_prepared_bcj_*"] },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/xz2-rs",
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
"https://github.com/jongiddy/bzip2-rs",
|
||||
]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/files/
|
||||
+9
-2
@@ -10,18 +10,25 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
attohttpc = "0.26.1"
|
||||
avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.0"
|
||||
rsa = "0.9.6"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = { version = "0.20.1", features = ["serde"] }
|
||||
toml_edit = { version = "0.21.0", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = "0.2.5"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
static = ["avbroot/static"]
|
||||
|
||||
+12
-50
@@ -1,64 +1,26 @@
|
||||
# End-to-end tests
|
||||
|
||||
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
|
||||
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some profiles for generating mock OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
|
||||
|
||||
For each image listed in the config, the test process will:
|
||||
For each profile listed in the config, the test process will:
|
||||
|
||||
1. Download the OTA if it doesn't already exist in `./files/<device>/` (or the workdir specified by `-w`)
|
||||
2. Verify the OTA checksum
|
||||
3. Run avbroot against the OTA using `--magisk`
|
||||
4. Extract the AVB-related partitions from the patched OTA and verify their checksums
|
||||
5. Verify the patched OTA checksum
|
||||
1. Generate a mock OTA based on the specification
|
||||
2. Verify tha original OTA checksum
|
||||
3. Run avbroot against the OTA using `--magisk` (with a mock Magisk APK)
|
||||
4. Verify the patched OTA checksum
|
||||
5. Extract the AVB-related partitions from the patched OTA
|
||||
6. Run avbroot against the OTA again using `--prepatched`
|
||||
7. Verify the patched OTA checksum again
|
||||
|
||||
For more efficient CI testing, the tests can operate on "stripped" OTAs. A stripped OTA is identical to the full OTA, except that partitions in `payload.bin` unrelated to AVB are zeroed out. This reduces the download size and disk space requirements by a couple orders of magnitude. **A stripped OTA is NOT bootable and should never be flashed on a real device.**
|
||||
The default profiles shipped with the project mimic how various stock OTAs for Pixel devices are built. The generated mock OTAs have valid signatures and data structures for all components, but without any actual data where possible. For example, most files in the ramdisks are empty files. To ensure the mock OTAs cannot be mistakenly installed on a real device, the OTA metadata lists a fake device name in the preconditions section.
|
||||
|
||||
## Running the tests
|
||||
|
||||
To test against the device OTA images listed in [`e2e.toml`](./e2e.toml), run:
|
||||
To test against the profiles listed in [`e2e.toml`](./e2e.toml), run:
|
||||
|
||||
```bash
|
||||
# To test all device OTAs
|
||||
# To test all profiles
|
||||
cargo run --release -- test -a
|
||||
# Or to test against specific device OTAs
|
||||
cargo run --release -- test -d cheetah -d bluejay
|
||||
# Or to test against specific profiles
|
||||
cargo run --release -- test -p pixel_v4_gki -p pixel_v4_non_gki
|
||||
```
|
||||
|
||||
To test against stripped OTAs (smaller download, but not bootable), pass in `--stripped`.
|
||||
|
||||
## Downloading a device image
|
||||
|
||||
To download a full OTA image, run:
|
||||
|
||||
```bash
|
||||
cargo run --release -- download -d <device>
|
||||
```
|
||||
|
||||
This normally happens automatically when running the `test` subcommand. To download the stripped OTA image instead, pass in `--stripped`.
|
||||
|
||||
If the image file does not already exist, then it will be downloaded and the checksums will be validated. If the download is interrupted, it will automatically resume when the command is rerun. If the file is already downloaded, the command is effectively a no-op unless `--revalidate` is passed in to revalidate the image checksums.
|
||||
|
||||
## Adding a new device image
|
||||
|
||||
To add a new device image to the testing configuration, run:
|
||||
|
||||
```bash
|
||||
cargo run --release -- add -d <device> -u <full OTA URL> -H <expected checksum>
|
||||
```
|
||||
|
||||
If the OS vendor does not provide a SHA-256 checksum, omit `-H` and the program will compute the checksum from the downloaded data.
|
||||
|
||||
This process will download the full OTA, strip it, patch the full OTA, patch the stripped OTA, extract the AVB partitions, and write all of the checksums to [`e2e.toml`](./e2e.toml).
|
||||
|
||||
The process for updating an existing device config is exactly the same as adding a new one.
|
||||
|
||||
## Stripping a full OTA
|
||||
|
||||
To convert a full OTA to the stripped form, run:
|
||||
|
||||
```bash
|
||||
cargo run --release -- strip -i <input zip> -o <output zip>
|
||||
```
|
||||
|
||||
This normally happens automatically as a part of adding a new device image.
|
||||
|
||||
+135
-103
@@ -1,116 +1,148 @@
|
||||
[magisk]
|
||||
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.3/Magisk.v26.3.apk"
|
||||
"hash" = "30ff6ec0709412adfcd0b735c0eb1f61cd9d589af4bdef4cf03c09b986b5acce"
|
||||
# Metadata used when generating OTAs. These values don't affect behavior at all.
|
||||
[ota_info]
|
||||
# Make sure generated OTAs aren't flashable on real devices.
|
||||
device = "avbroot_fake_device"
|
||||
fingerprint = "avbroot/avbroot_fake_device:14/UQ1A.240101.000/12345678:user/release-keys"
|
||||
build_number = "UQ1A.240101.000"
|
||||
incremental_version = "12345678"
|
||||
android_version = "14"
|
||||
sdk_version = "34"
|
||||
security_patch_level = "2024-01-01"
|
||||
|
||||
# Google Pixel 7 Pro
|
||||
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
|
||||
[device.cheetah]
|
||||
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq3a.230901.001-6b881553.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 152523 },
|
||||
{ start = 21750700, end = 23251455 },
|
||||
{ start = 2044485458, end = 2044493782 },
|
||||
{ start = 2316042420, end = 2333812318 },
|
||||
{ start = 2344911202, end = 2344916318 },
|
||||
]
|
||||
hash.original.full = "6b881553f012d582080642d660e1cf5c9e6fe41e9f1c6ab12ae87fab7894e307"
|
||||
hash.original.stripped = "9befd7887a125ebd8e9ae0555469dababe6bc04b0aa41aa2562036782a6d87e0"
|
||||
hash.patched.full = "91e15447ade648c10bce599e75569ce55edc18798b11a704586acf3b51ae7971"
|
||||
hash.patched.stripped = "84b069ac7f20115ac0b19d7e319bb86d00b9f3f64e0c7a51612f99d6cae297bb"
|
||||
hash.avb_images."init_boot.img" = "3bedb41be98c46241f11219021dfbb799a6d5c89e6e00d45a66f9a5b42e7dfbc"
|
||||
hash.avb_images."vbmeta.img" = "e8e6e898ca73807edb43af0a0e86d4a94b14256def89581970287a9b1bf7a3ee"
|
||||
hash.avb_images."vbmeta_system.img" = "dbb63e08f26f46ccda501d99058d513ff71e3d6302c14d587442b666ff08862a"
|
||||
hash.avb_images."vbmeta_vendor.img" = "6ffa0a10e72c3371653be80de1380832b4d7f8bbf38a2bd861d44a4097a57117"
|
||||
hash.avb_images."vendor_boot.img" = "dd58e8d46dd26198edf14f72d17a3315ff4c2aeb65b98dbd06369ca2a6e365a3"
|
||||
|
||||
[profile.pixel_v4_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v4"
|
||||
data.kernel = true
|
||||
|
||||
[profile.pixel_v4_gki.partitions.init_boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v4"
|
||||
data.ramdisks = ["init"]
|
||||
|
||||
[profile.pixel_v4_gki.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v4_gki.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "init_boot", "vendor_boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v4_gki.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v4_gki.partitions.vendor_boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "vendor_v4"
|
||||
data.ramdisks = ["otacerts"]
|
||||
|
||||
[profile.pixel_v4_gki.hashes]
|
||||
original = "f9477a35e3b60a495e49431c61e3897f11775f453a6a9897ead568357c963618"
|
||||
patched = "d3436650b4e0c60688dafb1472fa5fe95e67d19a8fad2da4850ffaa739d44574"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
[device.bluejay]
|
||||
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq3a.230901.001-1f1f0abe.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 142587 },
|
||||
{ start = 1062619, end = 21678441 },
|
||||
{ start = 1920893927, end = 1920902079 },
|
||||
{ start = 2103596124, end = 2126776858 },
|
||||
{ start = 2133324030, end = 2133329146 },
|
||||
]
|
||||
hash.original.full = "1f1f0abe67a6f6f47287be6dafec2c12628de6a715b82ca7beddaf67ad22aca5"
|
||||
hash.original.stripped = "38b15f5efdc7e056bc799859ba72ef9a73e93c61292c59f85fb4b9c31acc5f82"
|
||||
hash.patched.full = "65f7e29591fb48ad9c7c3233df4d76bb6986aef96b94c06b73213477bc7486d8"
|
||||
hash.patched.stripped = "cf77b5e307ce4d2a62e742cd3a300964de6c693880cbfa90dcd0bfec66bd1976"
|
||||
hash.avb_images."boot.img" = "a19cb4d4fcc7f3e7d3046c3d19e2f243fb02513ca848ff92ad70d1ada55c4e65"
|
||||
hash.avb_images."vbmeta.img" = "2d817e35f7b6cdc2edce58ef249a966fc677085b70a4aced4c829320aeef0be2"
|
||||
hash.avb_images."vbmeta_system.img" = "98a050f0d53a016fbb78147b1b4a9bca3fde615aa4da34bf62c2e07a395104b5"
|
||||
hash.avb_images."vbmeta_vendor.img" = "fac530f47f237e76f3c7c3cdfe96308170dd8e8f0b227d81114a489c69ba763c"
|
||||
hash.avb_images."vendor_boot.img" = "b3c596360f38cd0d6212341571acf3c2d977921f8bf163d7da27978a353da9f7"
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v4"
|
||||
data.kernel = true
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.vendor_boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "vendor_v4"
|
||||
data.ramdisks = ["init_and_otacerts", "dlkm"]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes]
|
||||
original = "021b4510bc244f5f686fbff89eb2058ec9c96a2949c2fe8caa7750a78d593225"
|
||||
patched = "0f90ae4c26a54a735d48e13e98bea73b6d1c026b0e95fa5b4b26179a1d6bdc86"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
[device.bramble]
|
||||
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq3a.230901.001-6d107ffa.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 142225 },
|
||||
{ start = 497945, end = 11647087 },
|
||||
{ start = 1683478307, end = 1683482723 },
|
||||
{ start = 1917281683, end = 1940628772 },
|
||||
{ start = 1943442548, end = 1943447665 },
|
||||
]
|
||||
hash.original.full = "6d107ffac1cd3da2c972112acc75957ed725e5c13d57ca724d9bcca5404fcebd"
|
||||
hash.original.stripped = "5b889bdab3bb12ddcd3c243a56e1c58bedada8831069f49d56fe5098fb141e35"
|
||||
hash.patched.full = "8725e03798539070d7075a07c80fb1403652a2b446d5c460d636a32b7368c9a2"
|
||||
hash.patched.stripped = "c4b5cfa84dc8c15f3ac9661d768062546fa36ca82cdeb6dd943347be0422903c"
|
||||
hash.avb_images."boot.img" = "8e7278a2e8ae44ffc5475717eb0e1aa56bfb7650aef34375b0fe92f790835f95"
|
||||
hash.avb_images."vbmeta.img" = "b036132b867f52a86eef79716261f35b1eb50e843dc4fe42f72cce67b24ae2db"
|
||||
hash.avb_images."vbmeta_system.img" = "9a7c6fd654e7a92aeffbdbd55ea0d87eee36f4c235e1b505423ad8a13a751a00"
|
||||
hash.avb_images."vendor_boot.img" = "6e83d22371af4a26aef2c64cc4235f83b03978b91aef69c34eec1985e9942139"
|
||||
|
||||
[profile.pixel_v3.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v3"
|
||||
data.kernel = true
|
||||
data.ramdisks = ["init"]
|
||||
|
||||
[profile.pixel_v3.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v3.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v3.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v3.partitions.vendor_boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "vendor_v3"
|
||||
data.ramdisks = ["otacerts"]
|
||||
|
||||
[profile.pixel_v3.hashes]
|
||||
original = "12221a69ff32e137d5f19b61f576fc6b33f0973c4a81da7722c640554ff4bc4e"
|
||||
patched = "0a92969bbd7cb30071a0799eb20028546d1cec3e6ec1ca4d7e1fe7776f1399fc"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
[device.sunfish]
|
||||
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq3a.230805.001-01fd34b2.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 131325 },
|
||||
{ start = 478553, end = 34014760 },
|
||||
{ start = 1658403115, end = 1658407531 },
|
||||
{ start = 1855778567, end = 1855783585 },
|
||||
]
|
||||
hash.original.full = "01fd34b206152a3559039161c9874ab03df37da4268b86a9e0be899de5fc0af7"
|
||||
hash.original.stripped = "cc311b5bd46e06cfdefbade794d33aa9bc3ceda4ad4f38bfe9f0dfc17033d207"
|
||||
hash.patched.full = "f2ac798b31a94dc251ca4ce370ebfb3073170d4a34431829df9ed0742149ffe7"
|
||||
hash.patched.stripped = "4387a5ba30c925f56c67eeaf6512757d5c1e07dfd2a8a99be14e06db8ba2dde7"
|
||||
hash.avb_images."boot.img" = "506a955080b6cfa2039ef85923e8a4e717ef6c1dc478538599c7ba26ee21e525"
|
||||
hash.avb_images."vbmeta.img" = "3679c7224e3e3e0793b4d1a031e116098460a6b4c5f3f88d5a3a0a4c65b21582"
|
||||
hash.avb_images."vbmeta_system.img" = "1d3efa00fd1d44a594c7317072468fa95c23d83d2759d6d6e757783ceeabc594"
|
||||
|
||||
# OnePlus 10 Pro
|
||||
# Build NE2215_11_C.26
|
||||
# What's unique:
|
||||
# - boot (boot v4) + recovery (boot v4)
|
||||
# - boot images have VTS signature block filled with all 0s
|
||||
# - payload.bin uses ZERO blocks
|
||||
# Build info:
|
||||
# - Unofficial list of full OTAs: https://forum.xda-developers.com/t/oneplus-10-pro-rom-ota-oxygen-os-repo-of-oxygen-os-builds.4572593/
|
||||
# - The North American builds are used because they're the only ones hosted on
|
||||
# a well known domain
|
||||
# - The build number can be found in <my_manifest>/build.prop since it's not
|
||||
# obvious from the filename
|
||||
[device.ossi]
|
||||
url = "https://android.googleapis.com/packages/ota-api/package/4cacbe5e6a3ab6a6fade68cc40f44d0fa6a2928a.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 204048 },
|
||||
{ start = 19105432, end = 34966775 },
|
||||
{ start = 2657405750, end = 2657407254 },
|
||||
{ start = 4984045446, end = 5006377281 },
|
||||
{ start = 5114504441, end = 5114507197 },
|
||||
{ start = 5138158449, end = 5138159817 },
|
||||
{ start = 5140101511, end = 5140105324 },
|
||||
]
|
||||
hash.original.full = "929f892fbd70699cf7f118a119aac1ae1b86351e1ada17715666fa4401e63472"
|
||||
hash.original.stripped = "4eabaf79b6c2b5df305e3ecdc2b9570c0dd27350b4e8d6434584000c4989ff3d"
|
||||
hash.patched.full = "ec576b7430e5a2788e89f8bbe37ee81b116386d9b101865d3b75eb9c51a0db4c"
|
||||
hash.patched.stripped = "ddd0cf1b42564f8c2980320c14f8e0eb125412eb7a13ae8fb52d35424546c235"
|
||||
hash.avb_images."boot.img" = "480d7cf519326fcaa5106ecfbbeb907309068635f7f6a25e5e4571d525c4926a"
|
||||
hash.avb_images."recovery.img" = "eeb0f67e2084174fced510f4b59a82022559ffbe1c20d2c0ea4757fcd989a3af"
|
||||
hash.avb_images."vbmeta.img" = "c022cf79da301a8430af5c49704944c490707fa0306031fe3ea22c39ce4734f6"
|
||||
hash.avb_images."vbmeta_system.img" = "749616b7f04487c05e9e363ad2071a0ab3bae29d497daf1f1a7695f7c8cfa82a"
|
||||
hash.avb_images."vbmeta_vendor.img" = "a6037fce745384425fb12745b8568386b84fb57ca6f94f6e47bcf754de341ae4"
|
||||
[profile.pixel_v2.partitions.boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "v2"
|
||||
data.kernel = true
|
||||
data.ramdisks = ["init_and_otacerts"]
|
||||
|
||||
[profile.pixel_v2.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v2.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v2.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes]
|
||||
original = "8b38d2d999b5b6e240e894f669e9e2643b3764c108d53bb7b02447da725e7c18"
|
||||
patched = "85b411947145e89cdc7f71b7109a12586f4dbddce3021f0f96172fc465c189d6"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQjwBqCb7mn4vtIbEE
|
||||
v/daiQICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAMdJ698WAn+aEvKOQ70wGjBIIJ
|
||||
UIJbh4gk0vu1YP5FAPR6S1jvfSaf+hCmDq1mrK2nUt1sv5vDUFimKAJUYs0lLwuH
|
||||
1FCB3QkXrn+2Z2wcJoeA1nq0pp39nvGu4EBBTAHgTebwfaqvfNvRE9kAjGjMWpux
|
||||
BKaXxpSOC1V2JNfcxWi+reKwOweDzde9s5vpvPx7/637GPrLiMOvKWoUSg15jDbF
|
||||
u8T/hfY2TC0KiRLLT9b/8DZrscjXH1jAM7e3FK9F1PT9DSO9SfEkH3YRGPwkcnrl
|
||||
JcI6aPD0J3/YtXYrgcTdNNUdJy3Tbq328peXyw1ayumQTHfHahCNaIaKtH2WmKtx
|
||||
xUcaZnFD+AxPT52Dl41XD69tN6tWSENuf0ruaD6HsRRcALH4X5xqLKsObA/t2b6S
|
||||
m3bWcbKhuD5MGLcuw0oKbDuEYVJeZvuppsKhr2JsqqPBbnJc8FR59t2IGMYOteFV
|
||||
HD6oX0YxIZU+Cb3R3xF7gO7cn/NxBJetph9FBhn1yLEqZnU2uWpU0+WFY64J8S/1
|
||||
yDJsu7Sa9HShZpUQfLC/eRsQQQKaZrUsCHHC9MFFqy9zmsn1lIFUbqAJ6MTe3cf4
|
||||
QQJNhwNdBzSInBuI8sv5lwk6eidQyyRkaB+M8TrjhIwFRlCBd7XUbPACID+m/1jD
|
||||
DvzB/iV8GHhDW0gf8jOObfGDA9zsDt9R1fVgb4ehimNPhDV8ILIELqREo9DWTr4Q
|
||||
RGmjSwiyENi1Pp4PXS12NI0KbdJ5zuj5scwwLogs3Kdt3WMmqCZU6ektDeKuRI0W
|
||||
ZnnCEiHo7jinMo869ZhyI3BQykuSfRH24JuPX+F3vxHmPoFYDjHG8ZCVWyZEMHPl
|
||||
wRxok5k8jwe+sMOmRtQZPkcJFSiSYWyUciRED2oV1U5ojDo2LBvmQLjgPwCj/ECo
|
||||
v9DwRzsrljwI7tpIqBmHbWxcSxdDhdQNiRk0PU0bunNh1ubJkoyj/FdEPHOvexpR
|
||||
hCGzwDRqGRz5Mk73FI6ybCbBguo7m3ic6duMNhSYQdKTBPAilPjSpEB+a4awyjFv
|
||||
sn5zVciwgti/CtZJiFDNeGAxr4V18OO8qdDVL8Za/SvqZrr0Y0MJgnWM4RuB6lBl
|
||||
d+Rwub9ReJIhiAjTPIiJfHibxtDLEtXEfbSdE/a/Yn0xQYUYHKdbYJS0heumSF7D
|
||||
ihvGedIpW16YjXCb052xCxeN05rDzbm1m7UJlFivSfo/GXgQSH4W9xzG8yLCid8L
|
||||
PnptMXJFbI53mCJfjKy7ZV7rlx6dXf/uN620qwr/QCxBQfjM4ndA7X3UUAF/riHW
|
||||
MgaY5+GcBhRhGug1IcIZG/p8QB0t8Lt6Xssu/87uVSsc9pKEymVZwdhy3gU/YZY0
|
||||
dx6AsYk9E2n05g5AtafsDnik1gVgBb5KtMzW7XKYAA8805Ms4+gqeS9OUrevF6QS
|
||||
mRFHJpvaNuEYR6Lp6jLfwPH6MK+tZbE42St8XGLbFFgp3ncSe6UBhoGCYN7/hJkw
|
||||
QXPnK7vlgiO3NEV4d0IdZWRPUHXASYiEkpv2IYmUq8CYyqZuBERVs6eZmLIC7D9V
|
||||
y6kORASxDl0wEl4Nc0B0iZ0jhMGiNmG9EO2ek+YwlpB+vBm1C/Z+30qpzAnbnh22
|
||||
HWHn5g9koMa4oEkO7P5Bpp4UifxX/S5f0lAuLotaE6eYmwr9Tw7N9Za/mSLk2CuK
|
||||
akc/T/ycgQdGb/wWHvCzvm3EsbuAAxFDqhwAoobhkdvSyQek8HYJ2FiiXHgFY9kI
|
||||
RJ49AHCaiXwHInZwVzJd30NJFn1I/o9dLULwYPNsjfeULr5ZUSe95CYLL4LUxDFr
|
||||
ecd0YiuoA1Y8gIwIRAaFAs1wdkNZoiEj3vH3LMFM6DMlvwDCB+vOpFwmLRQOT5Vq
|
||||
DWPZTKNYkmU9ReWY6AM2VhmMZa+GHW7wbPKllPUCA/OmZQM98f1ivPXhADP+IAWt
|
||||
1zNy+hqhrB/HV820t7m8SEK9GRAj0ARfpV6b9LsUH91TBY4MXKSZW9BDlu51Nwcl
|
||||
zRZDIW1ZDA4cCI8JWnCALTNntuzcRx+y9pBRwxSOJXv6i08yOxDE7En1iiF79S6T
|
||||
91jo20SLGeazQm1iun2eOh/49xplktxED5m/4/aAIjwDxXpbGT9VzOZ86P0/z32j
|
||||
G0PngN173UGkmLDUZlaUavVrR3l85Uw6OxIgNMDaG8gQkbZT+iVLQs4MGiOJqFfc
|
||||
UqusOXh2E3WUkX+5cWIdv3GGwBvKw6NE09u1+RJMvqHB3NlOruSazDyTVgPC/IkQ
|
||||
iMSm1rGWdXQozaAur3jXQDYrmM4iV2ydDDcUc4FAgCwnT68073BemHvv9gPzTyex
|
||||
Wr8N98nmTepypAcgARDI2wlwaGm3fO5kvaxOq4fiqYK7PoTUVsS6OCfafhR7MNVQ
|
||||
lVpNHaDFS99ygjzjIvd2tQYY/Mwsvhp6YyMcXhtqTk0snlCGNFQh1rnDfEUFCGV7
|
||||
EaraeuvTk5+ofTd9S8Qm+weYddRkHeZVpEFAOa/cL/cevek7yv60pXDUPrmlWD29
|
||||
Ydu0VEO7DmVQnLm2gDprIQsZc01QxKRpCuRgwRgcpmAHX+xCFUWTCFL59mHW3F5x
|
||||
7VCl79NXYaO6Rp7GV6xTgjD5FMe0pP51s8+dUwTCq3U5Y5KqScvvHiPaatb0DuSI
|
||||
FcMzXRcapGrj5C42U2R7cpLq0xJi5QFapDD+wlEUs0lxZDiqf30Qr197a3KOOsWs
|
||||
pyoR/Ytry0nyHxy4CFvA5hbWc7ng2ZegykCkBVS6XM2upUbWxbPihb3v5UgDb3zD
|
||||
wXXwhjs+hwIh6xha7HlfH43HF8ucjLsPadw5EuQfQJ21a4dmaMbSDu1u6aPoKHsQ
|
||||
zF/DnySx5ZEdjNz9xMvz7g8LoRAWR4dMsuZFOBismy7KmMqFNsw0GNSRcDT5ffZf
|
||||
MmXbI654Jv4s6UMl2ghyK7xmYiEeSeVH41AF/LaZMl4V5KSNKuOhn764DzB6z0d4
|
||||
1XQrq3wCpJ3K4hknKZhTbgSAmvk7i8CwpWZfUWAzuBL9jS1Z6RoN/zgsjM5vDMSs
|
||||
QUEguBjytPKioiuhZh+F+buvoNDKWvYwlJk8tUyNwtFOhWjniend7zEeioVO/kdg
|
||||
abSrBmjqZkxbCVreNdDekiGwOz1iD1dlreW6lJ6hW0mBnSAafR/lwXLJCE1slTWf
|
||||
LIbM8DLZMMeKZ9v7M+VMlW0uL36utP2hMnzHnh9f9eYG
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
CWokJ23olHitbyjqw6aZoIFhAo3mwzwdEEkv7sjh600hPwR0YNUDxSkNA7ztK5Ii
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE6jCCAtKgAwIBAgIIG/hHrySZ878wDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE
|
||||
AwwHYXZicm9vdDAgFw0yMzEyMjYwMzQwNDBaGA8yMDUxMDUxMzAzNDA0MFowEjEQ
|
||||
MA4GA1UEAwwHYXZicm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||
AK9VVEeI2UnjQr3IrVlZddq2lTHOlV8qFBVzMguVJUr128HqIXQRgXbEzHfCOOzC
|
||||
Nuh2x+cBXO7gztpwjSJFi2P+q4OdEujmgCf8SdwxS1NqWo5TRsB9szPALeQ+FQi8
|
||||
yir+urv6uJUuDpwwPgjhCDwn1tL426c4HgGsQpO0IWjCzc2SWL+KP4zirk7GIf2R
|
||||
Fy0TsFeJi5ucHWiMuXCGyiPZ2xV7AZ3AakZnf6rqWEIkOOsBf6UVNxFKuppMBcY6
|
||||
Lb6YcXB7nn2xFwZ+S5gGdZjkZIlLAoZjRynVRBz2BwlJB6ignH8+MbWkYS6ls9C8
|
||||
C10SmiGXI/S/DhRR11JCNAC5AD8FO+A6i9Vp19PiVmUv6nTx01+FhRqTeiW5bPhk
|
||||
PTn9gUAU5FKL4ouhr3ojJd6BczdJ2lWB9lQYmWnwzdYnSAnvXwc3WaSX+ryi7LoJ
|
||||
sZtlc91ZT2yNiqmPQpH6fcPKfi/BJZhe61hZ7Xv3+OVupbmU+IuUK9iKEw5+aqNt
|
||||
DiT15PKoyKuAfm0A/TJ1UV/P4FbSavEOrMse2SZvhRL/EvwpRXwaiwPFQL/vZ5GZ
|
||||
mGCXdmVt0FEH9z3UyYq9+UIhZoQh3V9eSrOgjOh3dnFZN5E/UQzqndykGy+pYjG4
|
||||
e4ZZQjQR5G3pmAiCIngqnqRUNWpG5DQcZYuXLdor19o5AgMBAAGjQjBAMB0GA1Ud
|
||||
DgQWBBQgy5IQsP4+QnQRWlMJiFsqwyXAzTAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
|
||||
DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAn8pexl/crAWBPJ/nPu9rhRo0
|
||||
dowLhbamhx/i5RRNxIJl8Lpf78Boj4t9ICuTU4yeLFHfA+IcnthS/wrFADbhSgF5
|
||||
m4gLBSfRjtiOPJSJqkIqCIV6RIcbfdCJa/V3p1nvu3vuGYEp9eSYACEtv6kLaMFu
|
||||
61RsEWgR6tNmb1Hssl9XzGdM2yf2vy/Gip+Ugztz9gfTF3Vhdos9VToPOzLNQh6C
|
||||
dr+uLKnnPHOb9PfAonYs23MAfpBaP8m4sEMkyMgACCUP+qld0S8xpptEA/9Wblxy
|
||||
zWJIZTNWhyfo7cH030lVbdgMBYtZN/WxYVtD3fp+BZPk5fD3QwM712Ja288anDic
|
||||
HRwtc0wFwJ9EOOwXT1kYT1GgKrmLTnmAGDktnjKtsHMvl7Rn2Wf+5XTQ3FC99DJU
|
||||
q/Jhe3BGUvOXS7uPfUSMQrH4q+cwUdPSLGu2TSPyXVUEv5/Z4mH4JD+HQbJrKjdO
|
||||
NvUlnNJgtL3nIW8iSR1BRkIQn9HAnHtfYELHFBr1XHdn8LCjeWmYStdI3s9jucWb
|
||||
5uIfij1/50hc6A5zl20/y1h9OGFKdRttjk9XzRR6HPt22zyStjab5Y5SzHOwMCaP
|
||||
wk/uDc+Aestyqm7BoIqU8XptC+VV92OoNiEouI0UI5zPw9EWLRmTjFAc4NzOc1I7
|
||||
2AKjtz3xnKC3R3oqtsA=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,54 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQ1ucIEwTFlt1U7QDz
|
||||
w8o/RAICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAqttk+qw4JJKTbU1zgKChgBIIJ
|
||||
ULyEdMcpMWYvgN6aefcIns6UJr3bYfq8aX0FvhGXzO1N8Voxg/jKc753Hj3F+h1d
|
||||
xfJyFzpg3D2jIIR3GRZrXILqc+aXaW3AS8/U7IN2d4gJIrYAbUelBnssdgLg6FSY
|
||||
MUprWGUC8ShMZpOhAoDD2V8ZHyb9NHJ/xPpbhhBUvDl/jQWurCo3KnWlFiXiGO3I
|
||||
VyH09LL0T+v4OcU7HjrVHWXokcv+6mkcPvkZZI6MTMC+7UyxNi9WlZs98Lzckpax
|
||||
asiEHImnDbtGsvYXvjKWn7QXs1CF4SVZ5DdoHMtf1BEHMnXTQ4teSRkp8B02ihr6
|
||||
LtSoStVXdw1E3+5R6DmW8b+Bxi7duO9URazkH2dHG7ZJvhL86DcvsFNnMW7NZ3PE
|
||||
N0BYDGTPjSAp8qbkCesbzhYEoDFsTfp6S1vLB+9PL0/S8jJ76MgZJ5f6i69aEz62
|
||||
Ek7pbydMnrMlMoWmgNEzcywGf1GnNcSQfPUVUyW11HLShNUoD+PktrnPRHhti2cr
|
||||
VXfWNeaf9lEwnQQYPGEuwxx4+lfK/sL6TV245ziVFAWQ0koJHIF09nIlFKGT9sOa
|
||||
JrwXWSMg1iT2iua4OLvwsHs7oMMvC+C4ycEMS0NKPEVx3pyAPrsO+rAHnEeUIz/D
|
||||
ooZEjnSfE9Xlsb5W4qqPuso4H9s/ulFsGz312ZEnt3EDGrgW2BksG42F7G3Qm8nn
|
||||
wUzSEGAAs/iMO/qbVF6P5/LnoO7h+ds9GCudmBYPKj1o5cwLcyNPjBHbKAXlzvOJ
|
||||
xyx17aCJ2f7uZzto1ov05lr6N+0/ajRwQn9IIx3R1wFvssETav+fXLavvm0Ul065
|
||||
iQgpyQnmZeKgzXClwmc9HgoFIMQSzs3lfRgV2coQCvFMgKvP6cVLzMf98NUf7XmM
|
||||
/fL6aG62KGUVSaat+4mfPT8p8MiuGBz2BmTkEtTWMf+HsgPu3yYKHJ6iAF4JjZ2m
|
||||
xpDOAd2jACpVVQ+7fxyejSl/VILFuJPL7RKAvIjJzq40X3o/w2Ydih1LgGLiyFHe
|
||||
I9RXAqCkrA7rbN/TEtZkgpgsLUbbY2b5+rd++DoCIzB4zF6JSE1RTRYkR4jsxyFB
|
||||
BFwNqgZX/xpA0vQ4WqpYkAc4tsefJEXxt2SsI4C3pVYLYuso+4hY0ZP+n3QXseyV
|
||||
N6VKpbztWleHXeLI53Bmb5kj4oJk44ouO36A/DtUlEESfvtGrnqRzVG8vMIqhOOU
|
||||
sX8L+XmvCSnCkV8zDqaI/Wmr/X+4BQMjWJ3C0v3RBXJNCPWaQG9laNoHrcdCxGuR
|
||||
GCNArOhyxHfjl7jplw2YgNscPPd/GFuUOY3UEG7hWW14/cpGBz5D/PfKnq1QkKw+
|
||||
e5xvq/+BYGch3K0kOh3aEhbgPZxztIF+JZMcSokNDMPATE7zPpCxAYFCkasA/ccJ
|
||||
CTeyZqvLGMFYuMwe/rnPbgcxr/1fEEzgAsYkC6JqsIFq2uhEaDPrvmQ9LwAAwBfw
|
||||
EWhHpSOtMxEwOvzOqGuKDqUy918f5xSXyvKGPA8RC2ZPEEVHChQfyRRcS/9TRqhR
|
||||
6E0h6frMunWUdBtiDmm3iC6hllWVmwmXJL68aOWokCOf0hBKLl0tGw2QKsaeD3ta
|
||||
SV8478YUoPn2oNZSQW4QwncQMU/6djjtsL31MF6itZU3ethXbxOQWO3RkybFY1iL
|
||||
2eZmExFd9IdagxrI8xOxxMq7hYbAF+gB1vgcuzktz3/3L/lBAAl6n+9eAAK3E87U
|
||||
jhNDC/flJWxpdbNmwGoAUmGMt+oemdtE2gqDpCMgU/TEpuJcPrNYf3cce/CFIYOG
|
||||
tsYYPzp1QE63418/A7+9nJYTC2Bqqc0nLnARVXKMUpAUuf5xnRNvMod50xMZviJi
|
||||
aMdGcKnMVAk0esJG68/W3cQdK1efIeIhd7pFYUq1WdzBwfJV3BUYbXLV40qld50U
|
||||
EuxUPxWVCKD0soPvRl6DqLY7fE4gzEykwzjc2Y6l8oLuQfAzaNX/ItEVX+qvUXKe
|
||||
myZpKYqAqjOOdAZHLfyyFQzIMHzls83XhTMixlY53ZNMn3rs34LYR0F2k7is6Ysh
|
||||
qxulqwQ/EcY446WQyTw3XT0IPLIIBXQGpYsuYChn/U2QVSoJxSS1LIeOpMSNJOwk
|
||||
QnsPtxB6gK3Svt/wdnK0GCZEKwMBx9VAz+M1NKCQQMsv2xWdQ4qm6fOhMEdm/oq5
|
||||
Pc9pQrq2J9XwBI/3AncKvjxKCfRC6Ob6gEO4cUybbEAZ9r3GCGtdf2+9x/aJm5XP
|
||||
OeXXRYbwOQtaQp5GgAHE/WoGPWt+lXKLu5mUqrbONS6TLibdAYve9l4cFp3hTvki
|
||||
159bCm4HrCUj3Etu3x6Nv5SMhXhRdyULrOvx9GA7kco2arXcxSPM79KiyCPMVvyu
|
||||
e/sRn3X/dS1NfaJ5IlKYLjiJQVySJOmyNhmlMQkLErqjQ1M2ibwybFglrUdJflcd
|
||||
Bjk3ZbN0r/BqbWWrwVIUVkq6vr0Yuxb1AgEl6HH1QNcX67XLSrKFG5sGkfsImMTd
|
||||
gvEYKFukgXmqKX3/p9czjJMYlC6sFT0L3e5G3LiEJlGT5jxklEitcAvwTowhbhCy
|
||||
T0Jf/KTNn2GnisDUK2DZKRbhiAHv3AEkxAIGg6On5SJ0PzFF0MhtxJ7nA3TqzvUN
|
||||
gN7jAAftaiALPImQf1EDgj3rHu8O8XAC/Nac65Qvn2ZUjMOW+Cba1POal0G76XZx
|
||||
JunWIDu0BgOXNc7kBRmE+dK9ppEbD5DScUjosAvkZuF94h23Ofzwm9/xBgXPbaV4
|
||||
CbWBtvRLuKacuLKF3WfQCw/Wi0K70dwDUEAaM1/rJiavGzkYp1z0KPey3hKDzkFL
|
||||
2ZjkzQdCreXo48Jwfwu4OQFTpkACWLvB0pLbhBXjq5Q9NEJHizRr08YT0gl+5mbJ
|
||||
Dk+Aw/n2Wy5ZRVbxxklO5brLnu1R8Rq3Szq1ATENrXSHwIN3sUiOEuNu8tSa3If3
|
||||
Hb5WS1WELgVPzGaUWKsWI9sooTJEnPOXxBq0U0iZpqpnoUw6NenIgIfBWrvG/7LC
|
||||
5SPksOVuaD6YdC4OF35/Jlrk4uKnCsW5SYa69Kr5RST/8MFGrefb7233iiRAo3La
|
||||
vr+uvh4NGA9Jf7Pv4yuN2IuCsyAl2jleSDEj6cf/tTz6m9xPT3Nr1/pKrL8mkRxv
|
||||
HDHq2ivR6QjBpEEoHITMaYndgOvwpCA1qXdsP/DiugCx
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
R9iItBaS278KvqlLyil1yUlizNGvWI93SERQ6uAitbzXm4fOc9t3c8dG8r2ThuZz
|
||||
+23
-126
@@ -3,44 +3,22 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{ffi::OsString, path::PathBuf, str::FromStr};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use avbroot::cli::args::{LogFormat, LogLevel};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct DeviceGroup {
|
||||
/// Device config name.
|
||||
pub struct ProfileGroup {
|
||||
/// OTA profile name.
|
||||
#[arg(short, long, value_name = "NAME")]
|
||||
pub device: Vec<String>,
|
||||
pub profile: Vec<String>,
|
||||
|
||||
/// All device configs.
|
||||
#[arg(short, long, conflicts_with = "device")]
|
||||
/// All profiles.
|
||||
#[arg(short, long, conflicts_with = "profile")]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct DownloadGroup {
|
||||
/// Revalidate hash of existing download.
|
||||
#[arg(long)]
|
||||
pub revalidate: bool,
|
||||
|
||||
/// Download the stripped OTA instead of the full OTA.
|
||||
#[arg(long)]
|
||||
pub stripped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct PatchGroup {
|
||||
/// Delete patched output files on success.
|
||||
#[arg(long)]
|
||||
pub delete_on_success: bool,
|
||||
|
||||
/// Suffix for patched output files.
|
||||
#[arg(long = "output-file-suffix", value_parser, default_value = ".patched")]
|
||||
pub suffix: OsString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ConfigGroup {
|
||||
/// Path to config file.
|
||||
@@ -53,112 +31,26 @@ pub struct ConfigGroup {
|
||||
)]
|
||||
pub config: PathBuf,
|
||||
|
||||
/// Working directory for storing images.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "DIRECTORY",
|
||||
value_parser,
|
||||
default_value = "files"
|
||||
)]
|
||||
pub work_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Convert a full OTA to stripped form.
|
||||
///
|
||||
/// A stripped OTA omits byte regions of the OTA that aren't needed for testing
|
||||
/// avbroot's patching logic (eg. the system partition image). This reduces the
|
||||
/// size of the test files by about two orders of magnitude.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct StripCli {
|
||||
/// Path to original OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
pub input: PathBuf,
|
||||
|
||||
/// Path to new stripped OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
pub output: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sha256Arg(pub [u8; 32]);
|
||||
|
||||
impl FromStr for Sha256Arg {
|
||||
type Err = hex::FromHexError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut data = [0u8; 32];
|
||||
hex::decode_to_slice(s, &mut data)?;
|
||||
Ok(Self(data))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new OTA image to the test config.
|
||||
///
|
||||
/// This will download the OTA image, strip it, patch both images, and add the
|
||||
/// resulting metadata (eg. checksums) to the specified test config file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddCli {
|
||||
/// URL to the full OTA zip.
|
||||
#[arg(short, long)]
|
||||
pub url: String,
|
||||
|
||||
/// Device config name.
|
||||
#[arg(short, long, value_name = "NAME")]
|
||||
pub device: String,
|
||||
|
||||
/// Expected sha256 hash of the full OTA zip.
|
||||
#[arg(short = 'H', long, value_name = "SHA256_HEX", value_parser)]
|
||||
pub hash: Option<Sha256Arg>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub patch: PatchGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
|
||||
/// Skip verifying OTA and AVB signatures.
|
||||
/// Working directory.
|
||||
///
|
||||
/// OTAs for some devices (eg. ossi) ship with vbmeta partitions containing
|
||||
/// invalid hashes. These will normally fail during validation.
|
||||
#[arg(long)]
|
||||
pub skip_verify: bool,
|
||||
}
|
||||
|
||||
/// Download a device image.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DownloadCli {
|
||||
/// Download the Magisk APK.
|
||||
#[arg(short, long)]
|
||||
pub magisk: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub device: DeviceGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub download: DownloadGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
/// If unset, a temporary directory is used, which will be automatically
|
||||
/// cleaned up, even if a failure occurs. Custom working directories are
|
||||
/// not deleted.
|
||||
#[arg(short, long, value_name = "DIRECTORY", value_parser)]
|
||||
pub work_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Run tests.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct TestCli {
|
||||
#[command(flatten)]
|
||||
pub device: DeviceGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub download: DownloadGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub patch: PatchGroup,
|
||||
pub profile: ProfileGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
}
|
||||
|
||||
/// List devices in config file.
|
||||
/// List profiles in config file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ListCli {
|
||||
#[command(flatten)]
|
||||
@@ -167,9 +59,6 @@ pub struct ListCli {
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
Strip(StripCli),
|
||||
Add(AddCli),
|
||||
Download(DownloadCli),
|
||||
Test(TestCli),
|
||||
List(ListCli),
|
||||
}
|
||||
@@ -178,4 +67,12 @@ pub enum Command {
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
|
||||
pub log_level: LogLevel,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
|
||||
+88
-97
@@ -3,15 +3,11 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{collections::BTreeMap, fs, ops::Range, path::Path};
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::{
|
||||
ser::ValueSerializer,
|
||||
visit_mut::{self, VisitMut},
|
||||
Array, Document, InlineTable, Item, KeyMut, Table, Value,
|
||||
};
|
||||
use toml_edit::Document;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Sha256Hash(
|
||||
@@ -23,110 +19,105 @@ pub struct Sha256Hash(
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Magisk {
|
||||
pub url: String,
|
||||
pub hash: Sha256Hash,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OtaInfo {
|
||||
pub device: String,
|
||||
pub fingerprint: String,
|
||||
pub build_number: String,
|
||||
pub incremental_version: String,
|
||||
pub android_version: String,
|
||||
pub sdk_version: String,
|
||||
pub security_patch_level: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct OtaHashes {
|
||||
pub full: Sha256Hash,
|
||||
pub stripped: Sha256Hash,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Avb {
|
||||
pub signed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RamdiskContent {
|
||||
Init,
|
||||
Otacerts,
|
||||
InitAndOtacerts,
|
||||
Dlkm,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BootVersion {
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
VendorV3,
|
||||
VendorV4,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ImageHashes {
|
||||
pub original: OtaHashes,
|
||||
pub patched: OtaHashes,
|
||||
pub avb_images: BTreeMap<String, Sha256Hash>,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BootData {
|
||||
pub version: BootVersion,
|
||||
#[serde(default)]
|
||||
pub kernel: bool,
|
||||
#[serde(default)]
|
||||
pub ramdisks: Vec<RamdiskContent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DmVerityContent {
|
||||
SystemOtacerts,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Device {
|
||||
pub url: String,
|
||||
pub sections: Vec<Range<u64>>,
|
||||
pub hash: ImageHashes,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DmVerityData {
|
||||
pub content: DmVerityContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VbmetaData {
|
||||
pub deps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Data {
|
||||
Boot(BootData),
|
||||
DmVerity(DmVerityData),
|
||||
Vbmeta(VbmetaData),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Hashes {
|
||||
pub original: Sha256Hash,
|
||||
pub patched: Sha256Hash,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Partition {
|
||||
pub avb: Avb,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Profile {
|
||||
pub partitions: BTreeMap<String, Partition>,
|
||||
pub hashes: Hashes,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub magisk: Magisk,
|
||||
pub device: BTreeMap<String, Device>,
|
||||
}
|
||||
|
||||
struct ConfigFormatter;
|
||||
|
||||
impl VisitMut for ConfigFormatter {
|
||||
fn visit_table_like_kv_mut(&mut self, key: KeyMut<'_>, node: &mut Item) {
|
||||
// Convert non-array-of-tables inline tables into regular tables.
|
||||
if let Item::Value(Value::InlineTable(t)) = node {
|
||||
let inline_table = std::mem::replace(t, InlineTable::new());
|
||||
*node = Item::Table(inline_table.into_table());
|
||||
}
|
||||
|
||||
// But for hashes, use dotted notation until TOML 1.1, which allows
|
||||
// newlines in inline tables, is released.
|
||||
if key == "hash" || key == "original" || key == "patched" || key == "avb_images" {
|
||||
if let Some(t) = node.as_table_like_mut() {
|
||||
t.set_dotted(true);
|
||||
}
|
||||
}
|
||||
|
||||
visit_mut::visit_table_like_kv_mut(self, key, node);
|
||||
}
|
||||
|
||||
fn visit_table_mut(&mut self, node: &mut Table) {
|
||||
// Make tables implicit unless they are empty, which may be meaningful.
|
||||
if !node.is_empty() {
|
||||
node.set_implicit(true);
|
||||
}
|
||||
|
||||
visit_mut::visit_table_mut(self, node);
|
||||
}
|
||||
|
||||
fn visit_array_mut(&mut self, node: &mut Array) {
|
||||
visit_mut::visit_array_mut(self, node);
|
||||
|
||||
// Put array elements on their own indented lines.
|
||||
if node.is_empty() {
|
||||
node.set_trailing("");
|
||||
node.set_trailing_comma(false);
|
||||
} else {
|
||||
for item in node.iter_mut() {
|
||||
item.decor_mut().set_prefix("\n ");
|
||||
}
|
||||
node.set_trailing("\n");
|
||||
node.set_trailing_comma(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a device to the config file. This leaves all comments intact, except for
|
||||
/// those contained within the existing device section if it exists.
|
||||
pub fn add_device(document: &mut Document, name: &str, device: &Device) -> Result<()> {
|
||||
let device_table = document.entry("device").or_insert_with(|| {
|
||||
let mut t = toml_edit::Table::new();
|
||||
t.set_implicit(true);
|
||||
Item::Table(t)
|
||||
});
|
||||
let old_table = device_table.get(name).and_then(|i| i.as_table());
|
||||
|
||||
let value = device.serialize(ValueSerializer::new())?;
|
||||
let Value::InlineTable(inline_table) = value else {
|
||||
unreachable!("Device did not serialize as an inline table");
|
||||
};
|
||||
let mut table = inline_table.into_table();
|
||||
|
||||
ConfigFormatter.visit_table_mut(&mut table);
|
||||
|
||||
// Keep top-level comment on the table.
|
||||
if let Some(t) = old_table {
|
||||
*table.decor_mut() = t.decor().clone();
|
||||
}
|
||||
|
||||
device_table[name] = Item::Table(table);
|
||||
|
||||
Ok(())
|
||||
pub ota_info: OtaInfo,
|
||||
#[serde(default)]
|
||||
pub profile: BTreeMap<String, Profile>,
|
||||
}
|
||||
|
||||
pub fn load_config(path: &Path) -> Result<(Config, Document)> {
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, Sender},
|
||||
},
|
||||
thread::{self, ThreadId},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use avbroot::stream::{PSeekFile, Reopen};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Minimum download chunk size per task.
|
||||
const MIN_CHUNK_SIZE: u64 = 1024 * 1024;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub trait ProgressDisplay {
|
||||
fn progress(&mut self, current: u64, total: u64);
|
||||
|
||||
fn error(&mut self, msg: &str);
|
||||
|
||||
fn finish(&mut self);
|
||||
}
|
||||
|
||||
pub struct BasicProgressDisplay {
|
||||
current: u64,
|
||||
total: u64,
|
||||
interval: Duration,
|
||||
last_render: Instant,
|
||||
avg: VecDeque<(Instant, u64)>,
|
||||
}
|
||||
|
||||
// Speed is a simple moving average over 5 seconds.
|
||||
static AVG_INTERVAL: Duration = Duration::from_millis(100);
|
||||
static AVG_WINDOW_SIZE: usize = 5000 / AVG_INTERVAL.as_millis() as usize;
|
||||
|
||||
impl BasicProgressDisplay {
|
||||
pub fn new(interval: Duration) -> Self {
|
||||
Self {
|
||||
current: 0,
|
||||
total: 0,
|
||||
interval,
|
||||
last_render: Instant::now() - interval,
|
||||
avg: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_line(&self) {
|
||||
eprint!("\x1b[2K\r");
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressDisplay for BasicProgressDisplay {
|
||||
fn progress(&mut self, current: u64, total: u64) {
|
||||
self.current = current;
|
||||
self.total = total;
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
if self.avg.is_empty() || (now - self.avg.back().unwrap().0) > AVG_INTERVAL {
|
||||
if self.avg.len() == AVG_WINDOW_SIZE {
|
||||
self.avg.pop_front();
|
||||
}
|
||||
|
||||
self.avg.push_back((now, current));
|
||||
}
|
||||
|
||||
if now - self.last_render > self.interval {
|
||||
let current_mib = current as f64 / 1024.0 / 1024.0;
|
||||
let total_mib = total as f64 / 1024.0 / 1024.0;
|
||||
|
||||
let front = self.avg.front().unwrap();
|
||||
let back = self.avg.back().unwrap();
|
||||
|
||||
let avg_window_mib = (back.1 - front.1) as f64 / 1024.0 / 1024.0;
|
||||
let avg_window_duration = back.0 - front.0;
|
||||
|
||||
let speed_mib_s = if avg_window_duration.is_zero() {
|
||||
0.0
|
||||
} else {
|
||||
avg_window_mib / avg_window_duration.as_secs_f64()
|
||||
};
|
||||
|
||||
self.clear_line();
|
||||
eprint!("{current_mib:.1} / {total_mib:.1} MiB ({speed_mib_s:.1} MiB/s)");
|
||||
|
||||
self.last_render = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn error(&mut self, msg: &str) {
|
||||
self.clear_line();
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
self.clear_line();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MessageData {
|
||||
Progress {
|
||||
bytes: u64,
|
||||
// Controller replies with a new ending offset.
|
||||
resp: Sender<u64>,
|
||||
},
|
||||
Completion {
|
||||
result: Result<()>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Message {
|
||||
id: ThreadId,
|
||||
data: MessageData,
|
||||
}
|
||||
|
||||
/// Download a contiguous byte range. The number of bytes downloaded per loop
|
||||
/// iteration will be sent to the specified channel via a `ProgressMessage`. The
|
||||
/// receiver of the message must reply with the new ending offset for this
|
||||
/// download via the oneshot channel in the `resp` field. An appropriate error
|
||||
/// will be returned if the full range (subject to modification) cannot be fully
|
||||
/// downloaded (eg. premature EOF is an error).
|
||||
fn download_range(
|
||||
url: &str,
|
||||
mut file: PSeekFile,
|
||||
initial_range: Range<u64>,
|
||||
channel: Sender<Message>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
assert!(initial_range.start < initial_range.end);
|
||||
|
||||
let mut response = attohttpc::get(url)
|
||||
.connect_timeout(TIMEOUT)
|
||||
.read_timeout(TIMEOUT)
|
||||
.header(
|
||||
"Range",
|
||||
&format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
|
||||
)
|
||||
.send()
|
||||
.and_then(|r| r.error_for_status())
|
||||
.with_context(|| format!("Failed to start download for range: {initial_range:?}"))?;
|
||||
|
||||
let mut range = initial_range.clone();
|
||||
let mut buf = [0u8; 65536];
|
||||
|
||||
while range.start < range.end {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
bail!("Received cancel signal");
|
||||
}
|
||||
|
||||
let to_read = (range.end - range.start).min(buf.len() as u64) as usize;
|
||||
let n = response.read(&mut buf[..to_read]).with_context(|| {
|
||||
format!(
|
||||
"Failed to download {to_read} bytes at offset {}",
|
||||
range.start,
|
||||
)
|
||||
})?;
|
||||
if n == 0 {
|
||||
bail!("Unexpected EOF from server");
|
||||
}
|
||||
|
||||
// This may overlap with another task's write when a range split occurs,
|
||||
// but the same data will be written anyway, so it's not a huge deal.
|
||||
file.seek(SeekFrom::Start(range.start))?;
|
||||
file.write_all(&buf[..n]).with_context(|| {
|
||||
format!(
|
||||
"Failed to write {n} bytes to output file at offset {}",
|
||||
range.start,
|
||||
)
|
||||
})?;
|
||||
|
||||
range.start += n as u64;
|
||||
|
||||
// Report progress to the controller.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let msg = Message {
|
||||
id: thread::current().id(),
|
||||
data: MessageData::Progress {
|
||||
bytes: n as u64,
|
||||
resp: tx,
|
||||
},
|
||||
};
|
||||
channel.send(msg)?;
|
||||
|
||||
// Get new ending offset from controller.
|
||||
let new_end = rx.recv()?;
|
||||
if new_end != range.end {
|
||||
debug_assert!(new_end <= range.end);
|
||||
range.end = new_end;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This just calls [`download_range()`] and sends a completion message to the
|
||||
/// channel with the result.
|
||||
fn download_thread(
|
||||
url: &str,
|
||||
file: PSeekFile,
|
||||
initial_range: Range<u64>,
|
||||
channel: mpsc::Sender<Message>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) {
|
||||
let result = download_range(url, file, initial_range, channel.clone(), cancel_signal);
|
||||
|
||||
channel
|
||||
.send(Message {
|
||||
id: thread::current().id(),
|
||||
data: MessageData::Completion { result },
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Send a HEAD request to get the value of the Content-Length header.
|
||||
fn get_content_length(url: &str) -> Result<u64> {
|
||||
let response = attohttpc::head(url)
|
||||
.connect_timeout(TIMEOUT)
|
||||
.read_timeout(TIMEOUT)
|
||||
.send()
|
||||
.and_then(|r| r.error_for_status())
|
||||
.context("Failed to send HEAD request to get Content-Length")?;
|
||||
|
||||
response
|
||||
.headers()
|
||||
.get("Content-Length")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.parse().ok())
|
||||
.ok_or_else(|| anyhow!("HEAD request did not return a valid Content-Length"))
|
||||
}
|
||||
|
||||
/// Download a set of file chunks in parallel. Only unrecoverable errors are
|
||||
/// returned as an Err. Normal/expected errors and download progress info are
|
||||
/// reported via `display`. Returns the remaining ranges that need to be
|
||||
/// downloaded.
|
||||
fn download_ranges(
|
||||
url: &str,
|
||||
output: &Path,
|
||||
initial_ranges: Option<&[Range<u64>]>,
|
||||
display: &mut dyn ProgressDisplay,
|
||||
max_threads: usize,
|
||||
max_errors: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
let file_size = get_content_length(url)?;
|
||||
|
||||
// Open for writing, but without truncation.
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
|
||||
file.set_len(file_size)
|
||||
.with_context(|| format!("Failed to set file size: {output:?}"))?;
|
||||
|
||||
// Queue of ranges that need to be downloaded.
|
||||
let mut remaining = VecDeque::from(match initial_ranges {
|
||||
Some(r) => r.to_vec(),
|
||||
#[allow(clippy::single_range_in_vec_init)]
|
||||
None => vec![0..file_size],
|
||||
});
|
||||
// Ranges that have failed.
|
||||
let mut failed = Vec::<Range<u64>>::new();
|
||||
// Ranges for currently running threads.
|
||||
let mut thread_ranges = HashMap::<ThreadId, Range<u64>>::new();
|
||||
|
||||
// Overall progress.
|
||||
let mut progress = file_size - remaining.iter().map(|r| r.end - r.start).sum::<u64>();
|
||||
display.progress(progress, file_size);
|
||||
|
||||
thread::scope(|scope| {
|
||||
let mut threads = HashMap::new();
|
||||
let mut error_count = 0u8;
|
||||
// Progress messages from threads.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
loop {
|
||||
// Spawn new threads.
|
||||
while !cancel_signal.load(Ordering::SeqCst) && threads.len() < max_threads {
|
||||
if remaining.is_empty() && !threads.is_empty() {
|
||||
// No more ranges to download. Split another thread's range.
|
||||
let (_, old_range) = thread_ranges
|
||||
.iter_mut()
|
||||
.max_by_key(|(_, r)| r.end - r.start)
|
||||
.unwrap();
|
||||
let size = old_range.end - old_range.start;
|
||||
|
||||
if size >= MIN_CHUNK_SIZE {
|
||||
let new_range = old_range.start + size / 2..old_range.end;
|
||||
old_range.end = new_range.start;
|
||||
remaining.push_back(new_range);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(thread_range) = remaining.pop_front() {
|
||||
// PSeekFile's reopen can't fail.
|
||||
let file_cloned = file.reopen().unwrap();
|
||||
let thread_range_cloned = thread_range.clone();
|
||||
let tx_cloned = tx.clone();
|
||||
|
||||
let join_handle = scope.spawn(|| {
|
||||
download_thread(
|
||||
url,
|
||||
file_cloned,
|
||||
thread_range_cloned,
|
||||
tx_cloned,
|
||||
cancel_signal,
|
||||
)
|
||||
});
|
||||
|
||||
thread_ranges.insert(join_handle.thread().id(), thread_range);
|
||||
threads.insert(join_handle.thread().id(), join_handle);
|
||||
} else {
|
||||
// No pending ranges and no running threads can be split.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if threads.is_empty() {
|
||||
// Nothing left to do.
|
||||
break;
|
||||
}
|
||||
|
||||
let Message { id, data } = rx.recv().unwrap();
|
||||
|
||||
match data {
|
||||
MessageData::Progress { bytes, resp } => {
|
||||
progress += bytes;
|
||||
display.progress(progress, file_size);
|
||||
|
||||
let thread_range = thread_ranges.get_mut(&id).unwrap();
|
||||
thread_range.start += bytes;
|
||||
|
||||
resp.send(thread_range.end).unwrap();
|
||||
}
|
||||
MessageData::Completion { result } => {
|
||||
threads.remove(&id).unwrap().join().unwrap();
|
||||
|
||||
let thread_range = thread_ranges.remove(&id).unwrap();
|
||||
|
||||
if let Err(e) = result {
|
||||
display.error(&format!("[{id:?}] {e:?}"));
|
||||
error_count += 1;
|
||||
|
||||
if error_count < max_errors {
|
||||
remaining.push_back(thread_range);
|
||||
} else {
|
||||
failed.push(thread_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
display.finish();
|
||||
|
||||
failed.extend(remaining);
|
||||
failed.extend(thread_ranges.into_values());
|
||||
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct State {
|
||||
ranges: Vec<Range<u64>>,
|
||||
}
|
||||
|
||||
fn read_state(path: &Path) -> Result<Option<State>> {
|
||||
let data = match fs::read_to_string(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to read download state: {path:?}"))?,
|
||||
};
|
||||
|
||||
let state = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse download state: {path:?}"))?;
|
||||
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
fn write_state(path: &Path, state: &State) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string(state).unwrap();
|
||||
|
||||
fs::write(path, data).with_context(|| format!("Failed to write download state: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_if_exists(path: &Path) -> Result<()> {
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
if e.kind() != io::ErrorKind::NotFound {
|
||||
return Err(e).context(format!("Failed to delete file: {path:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn state_path(path: &Path) -> PathBuf {
|
||||
let mut s = path.as_os_str().to_owned();
|
||||
s.push(".state");
|
||||
PathBuf::from(s)
|
||||
}
|
||||
|
||||
/// Download `url` to `output` with parallel threads.
|
||||
///
|
||||
/// If `initial_ranges` is specified, only those sections of the file will be
|
||||
/// downloaded. The empty regions are left untouched (i.e. filled with zeroes).
|
||||
/// A `.state` file is written if the download is interrupted. If the state
|
||||
/// file exists when this function is called, `initial_ranges` is ignored and
|
||||
/// the ranges from the state file are used to resume the download.
|
||||
pub fn download(
|
||||
url: &str,
|
||||
output: &Path,
|
||||
initial_ranges: Option<&[Range<u64>]>,
|
||||
display: &mut dyn ProgressDisplay,
|
||||
max_tasks: usize,
|
||||
max_errors: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let state_path = state_path(output);
|
||||
let ranges = match read_state(&state_path)? {
|
||||
Some(r) => Some(r.ranges),
|
||||
None => initial_ranges.map(|r| r.to_vec()),
|
||||
};
|
||||
|
||||
let remaining = download_ranges(
|
||||
url,
|
||||
output,
|
||||
ranges.as_deref(),
|
||||
display,
|
||||
max_tasks,
|
||||
max_errors,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
if remaining.is_empty() {
|
||||
delete_if_exists(&state_path)?;
|
||||
} else {
|
||||
write_state(&state_path, &State { ranges: remaining })?;
|
||||
bail!("Download was interrupted");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+1006
-614
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
id=com.chiller3.avbroot.clearotacerts
|
||||
name=clearotacerts
|
||||
version=v2.3.2
|
||||
versionCode=131842
|
||||
author=chenxiaolong
|
||||
description=Block A/B OTAs by clearing verification certificates
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/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
|
||||
@@ -1 +0,0 @@
|
||||
#MAGISK
|
||||
@@ -1,83 +0,0 @@
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
import android.os.Process;
|
||||
import android.system.ErrnoException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi", "SoonBlockedPrivateApi"})
|
||||
public class Main {
|
||||
private static final int GET_SERVICE_ATTEMPTS = 30;
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static IInterface getService(Class<?> interfaceClass, String serviceName) throws Exception {
|
||||
Class<?> serviceManager = Class.forName("android.os.ServiceManager");
|
||||
Method getService = serviceManager.getDeclaredMethod("getService", String.class);
|
||||
|
||||
Class<?> stub = Class.forName(interfaceClass.getCanonicalName() + "$Stub");
|
||||
Method asInterface = stub.getDeclaredMethod("asInterface", IBinder.class);
|
||||
|
||||
// ServiceManager.waitForService() tries to start the service, which we want to avoid to be
|
||||
// 100% sure we're not disrupting the boot flow.
|
||||
for (int attempt = 1; attempt <= GET_SERVICE_ATTEMPTS; ++attempt) {
|
||||
IBinder iBinder = (IBinder) getService.invoke(null, serviceName);
|
||||
if (iBinder != null) {
|
||||
return (IInterface) asInterface.invoke(null, iBinder);
|
||||
}
|
||||
|
||||
if (attempt < GET_SERVICE_ATTEMPTS) {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Service " + serviceName + " not found after " + GET_SERVICE_ATTEMPTS + " attempts");
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private static void unlock() throws Exception {
|
||||
Class<?> iOemLockService = Class.forName("android.service.oemlock.IOemLockService");
|
||||
IInterface iFace = getService(iOemLockService, "oem_lock");
|
||||
|
||||
Method setOemUnlockAllowedByUser = iOemLockService.getDeclaredMethod("setOemUnlockAllowedByUser", boolean.class);
|
||||
Method isOemUnlockAllowedByUser = iOemLockService.getDeclaredMethod("isOemUnlockAllowedByUser");
|
||||
|
||||
Boolean unlockAllowed = (Boolean) isOemUnlockAllowedByUser.invoke(iFace);
|
||||
if (unlockAllowed) {
|
||||
System.out.println("OEM unlocking already enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("Enabling OEM unlocking");
|
||||
setOemUnlockAllowedByUser.invoke(iFace, true);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"ConstantConditions", "JavaReflectionMemberAccess"})
|
||||
private static void switchToSystemUid() throws Exception {
|
||||
if (Process.myUid() != Process.SYSTEM_UID) {
|
||||
Method setUid = Process.class.getDeclaredMethod("setUid", int.class);
|
||||
int errno = (int) setUid.invoke(null, Process.SYSTEM_UID);
|
||||
|
||||
if (errno != 0) {
|
||||
throw new Exception("Failed to switch to SYSTEM (" + Process.SYSTEM_UID + ") user",
|
||||
new ErrnoException("setuid", errno));
|
||||
}
|
||||
if (Process.myUid() != Process.SYSTEM_UID) {
|
||||
throw new IllegalStateException("UID didn't actually change: " +
|
||||
Process.myUid() + " != " + Process.SYSTEM_UID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
switchToSystemUid();
|
||||
unlock();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to enable OEM unlocking");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
id=com.chiller3.avbroot.oemunlockonboot
|
||||
name=oemunlockonboot
|
||||
version=v2.3.2
|
||||
versionCode=131842
|
||||
author=chenxiaolong
|
||||
description=Enable OEM unlocking on every boot
|
||||
@@ -1,20 +0,0 @@
|
||||
exec >/data/local/tmp/avbroot_oem_unlock.log 2>&1
|
||||
|
||||
mod_dir=${0%/*}
|
||||
|
||||
header() {
|
||||
echo "----- ${*} -----"
|
||||
}
|
||||
|
||||
header Environment
|
||||
echo "Timestamp: $(date)"
|
||||
echo "Script: ${0}"
|
||||
echo "UID/GID/Context: $(id)"
|
||||
|
||||
header Enable OEM unlocking
|
||||
CLASSPATH="${mod_dir}/classes.dex" app_process / Main &
|
||||
pid=${!}
|
||||
wait "${pid}"
|
||||
echo "Exit status: ${?}"
|
||||
echo "Logcat:"
|
||||
logcat -d --pid "${pid}"
|
||||
+1
-10
@@ -11,14 +11,5 @@ publish = false
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
human-sort = "0.2.2"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = "0.20.1"
|
||||
walkdir = "2.3.3"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
default-features = false
|
||||
toml_edit = "0.21.0"
|
||||
|
||||
+1
-12
@@ -8,7 +8,7 @@ To update the project version, run:
|
||||
cargo xtask set-version -V <version>
|
||||
```
|
||||
|
||||
This will update the main [`Cargo.toml`](../Cargo.toml) and each module's `module.prop` file.
|
||||
This will update the main [`Cargo.toml`](../Cargo.toml) file.
|
||||
|
||||
## Updating changelog links
|
||||
|
||||
@@ -19,14 +19,3 @@ Then, run the following command to generate the appropriate link references:
|
||||
```bash
|
||||
cargo xtask update-changelog
|
||||
```
|
||||
|
||||
## Build modules
|
||||
|
||||
To build avbroot's companion modules, run:
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
# or -m <module1> -m <module2> ...
|
||||
```
|
||||
|
||||
See the main [`README.md`](../README.md#avbroot-magisk-modules) for more details.
|
||||
|
||||
+1
-4
@@ -4,13 +4,12 @@
|
||||
*/
|
||||
|
||||
mod changelog;
|
||||
mod module;
|
||||
mod version;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{module::ModulesCli, version::SetVersionCli};
|
||||
use crate::version::SetVersionCli;
|
||||
|
||||
const WORKSPACE_DIR: &str = env!("CARGO_WORKSPACE_DIR");
|
||||
|
||||
@@ -19,7 +18,6 @@ fn main() -> Result<()> {
|
||||
|
||||
match cli.command {
|
||||
Command::SetVersion(c) => version::set_version_subcommand(&c),
|
||||
Command::Modules(c) => module::modules_subcommand(&c),
|
||||
Command::UpdateChangelog => changelog::update_changelog_subcommand(),
|
||||
}
|
||||
}
|
||||
@@ -27,7 +25,6 @@ fn main() -> Result<()> {
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
SetVersion(SetVersionCli),
|
||||
Modules(ModulesCli),
|
||||
/// Update links in CHANGELOG.md.
|
||||
UpdateChangelog,
|
||||
}
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::{Args, Parser, ValueEnum};
|
||||
use tempfile::TempDir;
|
||||
use walkdir::WalkDir;
|
||||
use zip::{write::FileOptions, ZipWriter};
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
|
||||
#[cfg(unix)]
|
||||
const D8: &str = "d8";
|
||||
#[cfg(windows)]
|
||||
const D8: &str = "d8.bat";
|
||||
|
||||
fn newest_child_by_name(directory: &Path) -> Result<PathBuf> {
|
||||
let mut children = vec![];
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
let path = entry
|
||||
.path()
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.map_err(|e| anyhow!("Non-UTF-8 path: {e:?}"))?;
|
||||
|
||||
children.push(path);
|
||||
}
|
||||
|
||||
children.sort_by(|a, b| human_sort::compare(a, b));
|
||||
|
||||
children
|
||||
.pop()
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("{directory:?} has no children"))
|
||||
}
|
||||
|
||||
fn build_empty_zip(writer: &mut dyn Write) -> Result<()> {
|
||||
let mut writer = ZipWriter::new_streaming(writer);
|
||||
writer.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_dex(writer: &mut dyn Write, sources: &[&Path]) -> Result<()> {
|
||||
let sdk = env::var_os("ANDROID_HOME")
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("ANDROID_HOME must be set to the Android SDK path"))?;
|
||||
let build_tools = newest_child_by_name(&sdk.join("build-tools"))?;
|
||||
let platform = newest_child_by_name(&sdk.join("platforms"))?;
|
||||
let d8 = build_tools.join(D8);
|
||||
let android_jar = platform.join("android.jar");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
let mut process = Command::new("javac")
|
||||
.args(["-source", "1.8"])
|
||||
.args(["-target", "1.8"])
|
||||
.arg("-cp")
|
||||
.arg(android_jar)
|
||||
.arg("-d")
|
||||
.arg(temp_dir.path())
|
||||
.args(sources)
|
||||
.spawn()?;
|
||||
let status = process.wait()?;
|
||||
if !status.success() {
|
||||
bail!("javac failed with {status}");
|
||||
}
|
||||
|
||||
let mut class_files = vec![];
|
||||
|
||||
for entry in WalkDir::new(temp_dir.path()) {
|
||||
let entry = entry?;
|
||||
|
||||
if entry.path().extension() == Some(OsStr::new("class")) {
|
||||
class_files.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
|
||||
let mut process = Command::new(d8)
|
||||
.arg("--output")
|
||||
.arg(temp_dir.path())
|
||||
.args(class_files)
|
||||
.spawn()?;
|
||||
let status = process.wait()?;
|
||||
if !status.success() {
|
||||
bail!("d8 failed with {status}");
|
||||
}
|
||||
|
||||
let mut reader = File::open(temp_dir.path().join("classes.dex"))?;
|
||||
io::copy(&mut reader, writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_props(data: &str) -> Result<HashMap<String, String>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for line in data.split('\n') {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((k, v)) = line.split_once('=') else {
|
||||
bail!("Malformed line: {line:?}");
|
||||
};
|
||||
|
||||
result.insert(k.trim().to_owned(), v.trim().to_owned());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn start_module(
|
||||
dist_dir: &Path,
|
||||
common_dir: &Path,
|
||||
module_dir: &Path,
|
||||
) -> Result<(PathBuf, ZipWriter<BufWriter<File>>)> {
|
||||
let module_prop_raw = fs::read_to_string(module_dir.join("module.prop"))?;
|
||||
let module_prop = parse_props(&module_prop_raw)?;
|
||||
|
||||
let name = module_prop["name"].as_str();
|
||||
let version = module_prop["version"].as_str();
|
||||
let version = version.strip_prefix('v').unwrap_or(version);
|
||||
let zip_path = dist_dir.join(format!("{name}-{version}.zip"));
|
||||
|
||||
let raw_writer = File::create(&zip_path)?;
|
||||
let mut zip_writer = ZipWriter::new(BufWriter::new(raw_writer));
|
||||
|
||||
zip_writer.start_file(
|
||||
"META-INF/com/google/android/update-binary",
|
||||
FileOptions::default(),
|
||||
)?;
|
||||
io::copy(
|
||||
&mut File::open(common_dir.join("update-binary"))?,
|
||||
&mut zip_writer,
|
||||
)?;
|
||||
|
||||
zip_writer.start_file(
|
||||
"META-INF/com/google/android/updater-script",
|
||||
FileOptions::default(),
|
||||
)?;
|
||||
io::copy(
|
||||
&mut File::open(common_dir.join("updater-script"))?,
|
||||
&mut zip_writer,
|
||||
)?;
|
||||
|
||||
zip_writer.start_file("module.prop", FileOptions::default())?;
|
||||
zip_writer.write_all(module_prop_raw.as_bytes())?;
|
||||
|
||||
Ok((zip_path, zip_writer))
|
||||
}
|
||||
|
||||
pub fn modules_subcommand(cli: &ModulesCli) -> Result<()> {
|
||||
let modules_dir = Path::new(WORKSPACE_DIR).join("modules");
|
||||
let common_dir = modules_dir.join("common");
|
||||
let dist_dir = modules_dir.join("dist");
|
||||
|
||||
fs::create_dir_all(&dist_dir)?;
|
||||
|
||||
let modules = if cli.module.all {
|
||||
Module::value_variants()
|
||||
} else {
|
||||
&cli.module.module
|
||||
};
|
||||
|
||||
for module in modules {
|
||||
let module_dir = modules_dir.join(module.to_string());
|
||||
|
||||
let (path, mut writer) = start_module(&dist_dir, &common_dir, &module_dir)?;
|
||||
|
||||
match module {
|
||||
Module::ClearOtaCerts => {
|
||||
writer.start_file("system/etc/security/otacerts.zip", FileOptions::default())?;
|
||||
build_empty_zip(&mut writer)?;
|
||||
}
|
||||
Module::OemUnlockOnBoot => {
|
||||
writer.start_file("classes.dex", FileOptions::default())?;
|
||||
build_dex(&mut writer, &[&module_dir.join("Main.java")])?;
|
||||
|
||||
writer.start_file("service.sh", FileOptions::default())?;
|
||||
let mut reader = File::open(module_dir.join("service.sh"))?;
|
||||
io::copy(&mut reader, &mut writer)?;
|
||||
}
|
||||
}
|
||||
|
||||
writer.finish()?;
|
||||
|
||||
let path = path.canonicalize()?;
|
||||
println!("Built module: {path:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
#[value(rename_all = "lower")]
|
||||
enum Module {
|
||||
ClearOtaCerts,
|
||||
OemUnlockOnBoot,
|
||||
}
|
||||
|
||||
impl fmt::Display for Module {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_possible_value()
|
||||
.expect("no values are skipped")
|
||||
.get_name()
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
pub struct ModuleGroup {
|
||||
/// Name of module.
|
||||
#[arg(short, long)]
|
||||
module: Vec<Module>,
|
||||
|
||||
/// Build all modules.
|
||||
#[arg(short, long, conflicts_with = "module")]
|
||||
all: bool,
|
||||
}
|
||||
|
||||
/// Build companion modules.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ModulesCli {
|
||||
#[command(flatten)]
|
||||
module: ModuleGroup,
|
||||
}
|
||||
+1
-52
@@ -27,43 +27,6 @@ fn update_cargo_version(version: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_module_version(path: &Path, version: &str) -> Result<()> {
|
||||
let mut version_code = 0;
|
||||
|
||||
// 8 bits per version component.
|
||||
for piece in version.split('.') {
|
||||
let piece: u32 = piece.parse()?;
|
||||
version_code <<= 8;
|
||||
version_code |= piece;
|
||||
}
|
||||
|
||||
let raw_reader = File::open(path)?;
|
||||
let mut reader = BufReader::new(raw_reader);
|
||||
let mut result = String::new();
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
|
||||
let n = reader.read_line(&mut line)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if line.starts_with("version=") {
|
||||
result.push_str(&format!("version=v{version}\n"));
|
||||
} else if line.starts_with("versionCode=") {
|
||||
result.push_str(&format!("versionCode={version_code}\n"));
|
||||
} else {
|
||||
result.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(path, &result)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_changelog_version(version: &str) -> Result<()> {
|
||||
let path = Path::new(WORKSPACE_DIR).join("CHANGELOG.md");
|
||||
let raw_reader = File::open(&path)?;
|
||||
@@ -109,26 +72,12 @@ fn update_changelog_version(version: &str) -> Result<()> {
|
||||
|
||||
pub fn set_version_subcommand(cli: &SetVersionCli) -> Result<()> {
|
||||
update_cargo_version(&cli.version)?;
|
||||
|
||||
let modules_dir = Path::new(WORKSPACE_DIR).join("modules");
|
||||
|
||||
for entry in fs::read_dir(modules_dir)? {
|
||||
let entry = entry?;
|
||||
|
||||
if entry.file_type()?.is_dir() {
|
||||
let module_prop = entry.path().join("module.prop");
|
||||
if module_prop.exists() {
|
||||
update_module_version(&module_prop, &cli.version)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_changelog_version(&cli.version)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the version number in all Cargo.toml files and in the module metadata.
|
||||
/// Set the version number in all Cargo.toml files.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetVersionCli {
|
||||
/// Version number.
|
||||
|
||||
Reference in New Issue
Block a user