Compare commits

..

69 Commits

Author SHA1 Message Date
Andrew Gunnerson e105efb6d3 Version 3.2.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-14 18:26:14 -04:00
Andrew Gunnerson 2a7df104ed CHANGELOG.md: Add entry for PR #304
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-14 18:17:06 -04:00
Andrew Gunnerson 8e52a9cf8c Add support for cross-compiling to Android
The precompiled binaries are compiled for aarch64 API 31, which should
work for every device that avbroot supports.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-14 17:49:15 -04:00
Andrew Gunnerson f9343aa542 Version 3.2.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 19:50:01 -04:00
Andrew Gunnerson 0391d4e2c3 CHANGELOG.md: Add entry for PR #268
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 18:50:10 -04:00
Andrew Gunnerson c18800dc44 patch/boot: Add entry name/index to zip error messages
`specified file not found in archive` is a useless error message when
there's no additional context.

Issue: #301

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 18:50:02 -04:00
Andrew Gunnerson 2db90f83c2 Add support for upcoming Magisk Canary 27003
Magisk no longer puts both magisk64 and magisk32 in the ramdisk.
Instead, it just puts a single binary for the target ABI.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 18:50:01 -04:00
Andrew Gunnerson 34915e256f Merge pull request #300 from bugreportion/master
README.ru.md: update translation
2024-06-01 10:04:50 -04:00
Ivan Katrovsky 61392eb9d6 README.ru.md: update translation
Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-06-01 15:11:29 +03:00
Andrew Gunnerson 029cb4264e README.md: Move supported device list to issue tracker
This makes it a bit easier to keep updated and gather user comments.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-31 18:36:39 -04:00
Andrew Gunnerson 98a7fc811d Merge pull request #298 from bugreportion/master
README.ru.md: update translation
2024-05-31 18:25:32 -04:00
Ivan Katrovsky f4f4ec8e0d README.ru.md: update translation
Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-06-01 01:12:47 +03:00
Andrew Gunnerson 99b316f55e CHANGELOG.md: Add entry for PR #297
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-30 00:08:58 -04:00
Andrew Gunnerson 50ee90b61a cli/avb: Allow writing new TOML file when packing an image
Previously, there was no way to easily see the values of the recomputed
fields without unpacking the newly built image. This commit adds a new
`--output-info` option to the `avb pack` subcommand to write/overwrite a
new `avb.toml` file.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-30 00:04:05 -04:00
Andrew Gunnerson 2e8bd9f9d4 CHANGELOG.md: Add entry for PR #296
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-29 23:52:42 -04:00
Andrew Gunnerson 3fa96714c4 avb: Add support for building minimally sized image
Previously, for resizable images, the user had to guess an appropriate
size for the final image that could fit all the AVB metadata. This
commit adds a new `--recompute-size` option to the `avb pack` subcommand
to generate a minimally sized image.

Fixes: #294

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-29 23:42:43 -04:00
Andrew Gunnerson 7a2530a199 README.md: Strongly discourage using any device besides Google Pixels
OnePlus seems to be getting worse and worse with some devices not being
recoverable.

Issue: #290

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-29 21:20:24 -04:00
Andrew Gunnerson f84df86ef5 Version 3.2.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 18:27:38 -04:00
Andrew Gunnerson 0c064981dd CHANGELOG.md: Add entry for PR #293
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 18:26:43 -04:00
Andrew Gunnerson 5645183ecc Merge pull request #293 from chenxiaolong/bump-fec-limits
avb: Bump hashtree and FEC size limits to accommodate 8 GiB images
2024-05-24 18:25:36 -04:00
Andrew Gunnerson bc7358a8d9 avb: Bump hashtree and FEC size limits to accommodate 8 GiB images
Fixes: #291

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 18:20:28 -04:00
Andrew Gunnerson d47c14ab12 Switch to Rust 1.73.0's builtin div_ceil function
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 17:57:45 -04:00
Andrew Gunnerson f479fe1a08 Version 3.2.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:19:04 -04:00
Andrew Gunnerson df7b76bc59 CHANGELOG.md: Add entry for PR #289
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:13:27 -04:00
Andrew Gunnerson e342b93902 cli/ota: Build list of boot patchers directly in patch_subcommand()
This way, all of the patchers are constructed in the same place and we
don't have to pass their parameters through multiple functions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:11:11 -04:00
Andrew Gunnerson d7439e15ae Add support for adding AVB public key to DSU trusted keys
This allows the user to boot GSIs signed by the same key. The option is
disabled by default because some Android builds disable DSU support by
removing all keys to reduce the attack surface. We don't want to
reenable DSU support on these builds unless the user asks for it.

Closes: #286

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:07:16 -04:00
Andrew Gunnerson 0f16f30dfb CHANGELOG.md: Add entry for PR #288
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:47:26 -04:00
Andrew Gunnerson 178c025eca Fix clippy 1.78.0 lints
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:44:01 -04:00
Andrew Gunnerson d6ac94c430 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:25:26 -04:00
Andrew Gunnerson 2de260a66c CHANGELOG.md: Add entry for PR #287
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:06:49 -04:00
Andrew Gunnerson e9ba770a15 Switch to my fork of the bzip2-rs library
This includes a fix for yet another infinite-loop-on-drop bug in the
bzip2::write::BzDecoder implementation. This could be easily triggered
when interrupting an avbroot command while it is extracting a bzip2
compressed payload.bin chunk.

Fixes: #285

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 16:46:58 -04:00
Andrew Gunnerson 3940b31acb Version 3.1.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-02 19:19:43 -04:00
Andrew Gunnerson e87ee7c4ad CHANGELOG.md: Add entry for PR #279
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-02 19:19:21 -04:00
Andrew Gunnerson 0f8e30da37 Merge pull request #279 from chenxiaolong/mac-universal
ci.yml: Build universal binary for macOS
2024-05-02 19:17:31 -04:00
Andrew Gunnerson ae0863e319 ci.yml: Build universal binary for macOS
Fixes: #278

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-02 19:10:16 -04:00
Andrew Gunnerson 46662796a0 Version 3.1.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 17:47:09 -04:00
Andrew Gunnerson 237d4e9b11 CHANGELOG.md: Add entry for PR #277
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 17:46:37 -04:00
Andrew Gunnerson 5adff50b01 cli/ota: Fix incorrectly quoted output and clippy warning
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 14:31:53 -04:00
Andrew Gunnerson 905eb9245c CHANGELOG.md: Add entry for PR #276
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 14:30:49 -04:00
Andrew Gunnerson e22f9f5676 tests: Remove all binary test data files
In light of the recently discovered backdoor in the xz project where a
part of the malicious code was distributed in the test files, let's
remove all of our test files and generate them at runtime. While our
test files are very simple and consist mostly of zeros, someone who is
not very familiar with these binary formats would have a harder time
examining them and making sure they aren't malicious. With this change,
the data structures are now plainly visible in the test code.

The files generated at runtime are byte-for-byte identical to the test
files being removed. One can verify by comparing the sha512 checksums of
the files with the sha512 checksums hardcoded in the new test code.

Closes: #265

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 14:15:08 -04:00
Andrew Gunnerson 17162df1e7 Merge #273: README: Add Russian localization 2024-04-23 21:52:56 -04:00
Ivan Katrovsky b4ea4dfc9c Fix typo 2024-04-24 02:57:08 +03:00
Ivan Katrovsky 5572493acf Added Russian localization
The main page of the repository is now available in Russian.
2024-04-23 18:24:08 +03:00
Andrew Gunnerson 9d38748221 CHANGELOG.md: Add entry for PR #253
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-14 22:07:18 -04:00
Andrew Gunnerson fd0408dffe Use fastboot flashall to flash partitions initially
`fastboot flashall` is identical to the `fastboot update` command used
by the Pixel factory images, except it reads from a directory instead of
a zip file. It knows how to flip between the fastboot and fastbootd
modes without user intervention.

Fixes: #252

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-14 21:59:30 -04:00
Andrew Gunnerson af0fb1d9b4 Update Github Actions dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-14 18:21:54 -04:00
Andrew Gunnerson 8c965552e8 Merge pull request #269 from schnatterer/feature/extend-docs
README: Add minor clarifications
2024-04-07 17:36:11 -04:00
Johannes Schnatterer 2532476583 README: Add minor clarifications 2024-04-07 21:32:42 +02:00
Andrew Gunnerson cf77999d95 README.md: Fix grammar
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-12 18:12:02 -04:00
Andrew Gunnerson 0ade50f49e README.md: Remove mention of my_engineering image
Patching the image was never implemented because it's specific to
OnePlus devices where the bootloader does not respect the custom root of
trust.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-12 17:54:39 -04:00
Andrew Gunnerson 0bad6f8c6f Version 3.1.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-05 21:59:41 -05:00
Andrew Gunnerson 4ce4863237 CHANGELOG.md: Add entry for PR #261
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-05 21:59:07 -05:00
Andrew Gunnerson f615bf48cc crypto: reformat_pem: Strip out irrelevant lines
x509_cert is unable to parse files that contains non-empty lines outside
of the BEGIN CERTIFICATE and END CERTIFICATE markers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-05 21:42:02 -05:00
Andrew Gunnerson b505b14574 CHANGELOG.md: Add entry for PR #257
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 20:38:39 -05:00
Andrew Gunnerson e2adff6eed hashtree: Precompute salted SHA-256 context
With the dm-verity hash tree format, each digest is salted, meaning it
first hashes the salt byte string and then the actual data. Previously,
the salt was being rehashed for each digest operation. Instead, the
salted SHA-256 context can be computed once and then cloned when needed.

The performance improvement is pretty minor, but it's worth doing anyway
since it's not any more complex.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 20:20:15 -05:00
Andrew Gunnerson 10370692d0 Version 3.1.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:51:51 -05:00
Andrew Gunnerson 73b6e9b177 CHANGELOG.md: Add entry for PR #256
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:51:15 -05:00
Andrew Gunnerson 8d25e28ea3 Fix clippy warnings and formatting
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:36:20 -05:00
Andrew Gunnerson 82fc1336f7 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:32:59 -05:00
Andrew Gunnerson 953c0c1c5b CHANGELOG.md: Add entry for PR #251
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:23:35 -05:00
Andrew Gunnerson c3b073cece Add tracing support
Instead of println'ing everything, this commit switches the code base to
using the tracing library. There are now proper log levels and multiple
logging output formats. A bunch of new debug and trace-level messages
have also been added to help with future troubleshooting.

By default, the output is kept nice and short. Spans won't be shown
unless the log level is set to debug or lower.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:16:57 -05:00
Andrew Gunnerson 565efc5ee5 CHANGELOG.md: Add entry for PR #255
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 14:58:39 -05:00
Andrew Gunnerson 6ef8e548a3 Add support for Magisk v27.0
Upstream Magisk now xz-compresses files in modifies in the ramdisk. This
commit also implements the same in avbroot's MagiskRootPatcher.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 14:35:57 -05:00
Andrew Gunnerson 80549346ea README.md: Temporarily suggest manual procedure for initially flashing system.img
We'll switch to using `fastboot flashall` in the future once that has
been implemented and tested.

Fixes: #252

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-01-31 17:39:13 -05:00
Andrew Gunnerson 275b18b2a5 CHANGELOG.md: Add entry for PR #247
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-31 16:29:40 -05:00
Andrew Gunnerson 0a68966594 Switch from xz2 to liblzma
liblzma is a maintained fork of xz2. We can now get rid of our own fork
of xz2.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-31 16:20:30 -05:00
Andrew Gunnerson 9ef5629556 CHANGELOG.md: Add entry for PR #246
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-30 19:25:49 -05:00
Andrew Gunnerson 1db09eb896 Merge pull request #246 from chenxiaolong/remove_modules
Remove oemunlockonboot module
2023-12-30 19:24:06 -05:00
Andrew Gunnerson a134d7f889 Remove oemunlockonboot module
The module has been split out into another repo [1] so that it can be
versioned separately. It now also supports Magisk's automatic update
mechanism.

Closes: #235

[1] https://github.com/chenxiaolong/OEMUnlockOnBoot

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-30 19:16:05 -05:00
58 changed files with 2908 additions and 1546 deletions
+96 -38
View File
@@ -13,24 +13,60 @@ concurrency:
jobs:
build:
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.artifact.os }}
env:
CARGO_TERM_COLOR: always
# https://github.com/rust-lang/rust/issues/78210
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
ANDROID_API: ${{ matrix.artifact.android_api }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
artifact:
- os: ubuntu-latest
name: x86_64-unknown-linux-gnu
- os: windows-latest
name: x86_64-pc-windows-msvc
- os: macos-latest
name: universal-apple-darwin
targets:
- aarch64-apple-darwin
- x86_64-apple-darwin
combine: lipo
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
- os: ubuntu-24.04
name: aarch64-linux-android31
targets:
- aarch64-linux-android
android_api: '31'
steps:
- name: Check out repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# For git describe
fetch-depth: 0
- name: Install qemu-user-static
if: ${{ contains(matrix.artifact.name, 'android') }}
shell: bash
run: |
sudo apt-get -y update
sudo apt-get -y install qemu-user-static
- name: Set Android temporary directory
if: ${{ contains(matrix.artifact.name, 'android') }}
shell: bash
run: |
echo "TMPDIR=/tmp" >> "${GITHUB_ENV}"
- name: Install cargo-android
shell: bash
run: |
cargo install \
--git https://github.com/chenxiaolong/cargo-android \
--tag v0.1.1
- name: Get version
id: get_version
shell: bash
@@ -40,14 +76,12 @@ jobs:
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
>> "${GITHUB_OUTPUT}"
- name: Get Rust target triple
id: get_target
- name: Install toolchains
shell: bash
env:
RUSTC_BOOTSTRAP: '1'
run: |
echo -n 'name=' >> "${GITHUB_OUTPUT}"
rustc -vV | sed -n 's|host: ||p' >> "${GITHUB_OUTPUT}"
for target in ${TARGETS}; do
rustup target add "${target}"
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
@@ -55,51 +89,75 @@ jobs:
- name: Clippy
shell: bash
run: |
cargo clippy --release --workspace --features static \
--target ${{ steps.get_target.outputs.name }}
for target in ${TARGETS}; do
cargo android \
clippy --release --workspace --features static \
--target "${target}"
done
- name: Build
shell: bash
run: |
cargo build --release --workspace --features static \
--target ${{ steps.get_target.outputs.name }}
for target in ${TARGETS}; do
cargo android \
build --release --workspace --features static \
--target "${target}"
done
- name: Tests
shell: bash
run: |
cargo test --release --workspace --features static \
--target ${{ steps.get_target.outputs.name }}
for target in ${TARGETS}; do
cargo android \
test --release --workspace --features static \
--target "${target}"
done
- 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
for target in ${TARGETS}; do
cargo android \
run --release -p e2e --features static \
--target "${target}" \
-- test -a -c e2e/e2e.toml
done
- name: Archive documentation
uses: actions/upload-artifact@v3
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
path: |
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
- name: Create output 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.
case "${{ matrix.artifact.combine }}" in
lipo)
mkdir target/output
cmd=(lipo -output target/output/avbroot -create)
for target in ${TARGETS}; do
cmd+=("target/${target}/release/avbroot")
done
"${cmd[@]}"
;;
'')
ln -s "${TARGETS}/release" target/output
;;
*)
echo >&2 "Unsupported combine argument"
exit 1
;;
esac
# This is done to ensure a flat directory structure. The upload-artifact
# action no longer allows multiple uploads to the same destination.
- name: Copy documentation to target directory
shell: bash
run: cp LICENSE README.md target/output/
- name: Archive executable
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
path: |
target/output/LICENSE
target/output/README.md
target/output/avbroot
target/output/avbroot.exe
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@v1
-35
View File
@@ -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/
+2 -4
View File
@@ -25,14 +25,12 @@ jobs:
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Check out repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: v${{ steps.get_version.outputs.version }}
name: Version ${{ steps.get_version.outputs.version }}
body_path: RELEASE.md
draft: true
prerelease: false
+76 -1
View File
@@ -7,6 +7,55 @@
to update the actual links at the bottom of the file.
-->
### Version 3.2.3
* Add prebuilt binary for Android (aarch64) ([PR #304])
### Version 3.2.2
* Add new `--recompute-size` option to `avbroot avb pack` to automatically recompute the image size for resizable images ([Discussion #294], [PR #296])
* Add new `--output-info` option to `avbroot avb pack` to write a new `avb.toml` file containing computed values ([PR #297])
* Add support for upcoming Magisk Canary 27003 ([Issue #301], [PR #268])
### Version 3.2.1
* Increase hash tree and FEC size limits to accommodate partition images up to 8 GiB ([Issue #291], [PR #293])
### Version 3.2.0
* Fix potential infinite loop when interrupting avbroot at the right moment to a bug in the bzip2-rs library ([Issue #285], [PR #287])
* Update all dependencies and fix new clippy lints ([PR #288])
* Add support for adding the custom AVB public key to the list of trusted keys for DSU (booting signed GSIs) ([Discussion #286], [PR #289])
### Version 3.1.3
* Build universal binary for macOS ([Issue #278], [PR #279])
### Version 3.1.2
* Use `fastboot flashall` for initial setup to avoid needing to manually flash every partition ([PR #253])
* Remove binary test files in the git repo and generate them at runtime ([Issue #265], [PR #276])
* Fix portions of a couple error messages being incorrectly quoted ([PR #277])
### 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:
@@ -140,6 +189,9 @@ 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
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
[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
@@ -154,6 +206,11 @@ Behind-the-scenes changes:
[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
[Issue #265]: https://github.com/chenxiaolong/avbroot/issues/265
[Issue #278]: https://github.com/chenxiaolong/avbroot/issues/278
[Issue #285]: https://github.com/chenxiaolong/avbroot/issues/285
[Issue #291]: https://github.com/chenxiaolong/avbroot/issues/291
[Issue #301]: https://github.com/chenxiaolong/avbroot/issues/301
[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
@@ -207,7 +264,6 @@ Behind-the-scenes changes:
[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 #220]: https://github.com/chenxiaolong/avbroot/pull/220
[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
@@ -225,3 +281,22 @@ Behind-the-scenes changes:
[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 #253]: https://github.com/chenxiaolong/avbroot/pull/253
[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
[PR #268]: https://github.com/chenxiaolong/avbroot/pull/268
[PR #276]: https://github.com/chenxiaolong/avbroot/pull/276
[PR #277]: https://github.com/chenxiaolong/avbroot/pull/277
[PR #279]: https://github.com/chenxiaolong/avbroot/pull/279
[PR #287]: https://github.com/chenxiaolong/avbroot/pull/287
[PR #288]: https://github.com/chenxiaolong/avbroot/pull/288
[PR #289]: https://github.com/chenxiaolong/avbroot/pull/289
[PR #293]: https://github.com/chenxiaolong/avbroot/pull/293
[PR #296]: https://github.com/chenxiaolong/avbroot/pull/296
[PR #297]: https://github.com/chenxiaolong/avbroot/pull/297
[PR #304]: https://github.com/chenxiaolong/avbroot/pull/304
Generated
+441 -342
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.0.0"
version = "3.2.3"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+4
View File
@@ -32,6 +32,10 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
### Repacking an AVB image
```bash
+40 -35
View File
@@ -1,5 +1,7 @@
# avbroot
(This page is also available in: [Russian (Русский)](./README.ru.md).)
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
@@ -13,9 +15,9 @@ 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 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.
* 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.
* 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)).
A list of devices known to work can be found in the issue tracker at [#299](https://github.com/chenxiaolong/avbroot/issues/299).
## Patches
@@ -25,7 +27,7 @@ avbroot applies the following patches to the partition images:
* 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.
* The `system` image 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
@@ -33,11 +35,7 @@ avbroot applies the following patches to the partition images:
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.
@@ -130,12 +128,16 @@ When patching OTAs for multiple devices, generating unique keys for each device
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (including a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (triggering a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
## Initial setup
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
```bash
fastboot flashing unlock
```
2. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
3. Extract the partition images from the patched OTA that are different from the original.
@@ -143,25 +145,26 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
```bash
avbroot ota extract \
--input /path/to/ota.zip.patched \
--directory extracted
--directory extracted \
--fastboot
```
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
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:
```bash
for image in extracted/*.img; do
partition=$(basename "${image}")
partition=${partition%.img}
fastboot flash "${partition}" "${image}"
done
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
```
5. Set up the custom AVB public key in the bootloader.
Note that this only flashes the OS partitions. The bootloader and modem/radio partitions are left untouched due to fastboot limitations. If they are not already up to date or if unsure, after fastboot completes, follow the steps in the [updates section](#updates) to sideload the patched OTA once. Sideloading OTAs always ensures that all partitions are up to date.
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
5. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
```bash
fastboot reboot-bootloader
fastboot erase avb_custom_key
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
```
@@ -182,14 +185,18 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
7. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
```bash
fastboot flashing lock
```
Confirm by pressing volume down and then power. Then reboot.
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.
@@ -251,16 +258,6 @@ To safely use Repair Mode:
Because the unrooting and rooting are done by flashing OTAs, the device's data will not be wiped.
## avbroot modules
avbroot's Magisk/KernelSU modules can be downloaded from the [releases page](https://github.com/chenxiaolong/avbroot/releases).
### `oemunlockonboot`: Enable OEM unlocking on every boot
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.
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
## Magisk preinit device
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition to be explicitly specified via `--magisk-preinit-device <name>`. To find the partition name:
@@ -366,6 +363,12 @@ The only behavior this changes is where the partition is read from. When using `
This has no impact on what patches are applied. For example, when using Magisk, the root patch is applied to the boot partition, no matter if the partition came from the original `payload.bin` or from `--replace`.
### Booting signed GSIs
Android's [Dynamic System Updates (DSU)](https://developer.android.com/topic/dsu) feature uses a different root of trust than the regular system. Instead of using the bootloader's `avb_custom_key`, it obtains the trusted keys from the `first_stage_ramdisk/avb/*.avbpubkey` files inside the `init_boot` or `vendor_boot` ramdisk. These files are encoded in the same binary format as `avb_pkmd.bin`.
avbroot can add the custom AVB public key to this directory by passing in `--dsu` when patching an OTA. This allows booting [Generic System Images (GSI)](https://developer.android.com/topic/generic-system-image) signed by the custom AVB key.
### Clearing vbmeta flags
Some Android builds may ship with a root `vbmeta` image with the flags set such that AVB is effectively disabled. When avbroot encounters these images, the patching process will fail with a message like:
@@ -437,13 +440,15 @@ 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:
### Android cross-compilation
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
```bash
cargo xtask modules -a
cargo android build --release --target aarch64-linux-android
```
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable must be set to the Android SDK path.
It is possible to run the tests if the host is running Linux, qemu-user-static is installed, and the executable is built with `RUSTFLAGS=-C target-feature=+crt-static` and `--features static`.
## Verifying digital signatures
+460
View File
@@ -0,0 +1,460 @@
# avbroot
avbroot – это программа для модификации OTA-образов Android A/B-формата с целью получения root-прав при сохранении прохождения AVB (Android Verified Boot) с использованием кастомных (пользовательских) ключей подписи. Она совместима как с Magisk, так и с KernelSU. При необходимости можно просто переподписать OTA, без получения root-доступа.
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
**ПРИМЕЧАНИЕ:** avbroot 2.0 была переписана на Rust и больше не имеет в основе никакого кода AOSP, а CLI полностью обратно совместим. Тем не менее, старую реализацию на Python можно найти в одноименной ветке `python`.
## Требования
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
* наличие файла `payload.bin` (обычно находится в корне архива)
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это производится с помощью команды `fastboot flash avb_custom_key`.
Список девайсов, на которых проверялась совместимость с указанным выше функционалом, находится здесь: [#299.](https://github.com/chenxiaolong/avbroot/issues/299)
## Патчи
avbroot модифицирует следующие образы:
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа. В случае с Magisk, патч будет эквивалентен тому, что производится в самом приложении Magisk.
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
* `system` тоже модифицируется для замены сертификата проверки подписи OTA. Это не позволит системному приложению обновлений ОС установить оригинальный непропатченный OTA и дает возможность использовать сторонние приложения для установки пропатченных OTA.
## Предостережения
* **Всегда оставляйте опцию `Заводской разблокировки`** (или OEM unlocking в англ.) **включенной при наличии root-прав с заблокированным загрузчиком.** Это очень важно. Доступ к root-правам потенциально позволяет перезаписать загрузочный раздел из-под системы, будь то сделано случайно или намеренно, файлом, который не был подписан должным образом. В таком случае, система и режим Recovery больше не смогут загрузиться, а команда `fastboot flashing unlock` будет недоступна, потому что параметр Заводской разблокировки отключен. То есть, это приведет к **_хардбрику устройства_**.
Повторюсь: **_ВСЕГДА оставляйте `Заводскую разблокировку` включенной при наличии root-прав._**
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). Это же относится и к методу `Прямой установки` для обновления Magisk. Обновление Magisk **должно выполняться только путем обновления OTA,** а не через Magisk Manager.
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв Issue,](https://github.com/chenxiaolong/avbroot/issues/new) и четко разъясните, какие конкретные действия привели к возникновению такой ситуации. Если Android всё еще работает и доступ к root-правам сохранился – вероятно, получится откатить изменения до исходного состояния, не стирая ваши данные.
## Использование
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
avbroot – это отдельный исполняемый файл. Он не требует установки и может быть запущен из любого места на диске.
3. [Сгенерируйте ключи подписи.](#генерация-ключей)
4. Пропатчите ОТА-архив с помощью команды:
```bash
avbroot ota patch \
--input /путь/к/ota.zip \
--key-avb /путь/к/avb.key \
--key-ota /путь/к/ota.key \
--cert-ota /путь/к/ota.crt \
```
Добавьте следующие аргументы в конец команды в зависимости от того, как вы хотите получить root-доступ.
* Для получения root-доступа с использованием Magisk:
```bash
--magisk /путь/к/magisk.apk \
--magisk-preinit-device <имя>
```
Если вы не знаете имени раздела предварительной инициализации Magisk, следуйте инструкции [в соответствующем разделе.](#предварительная-инициализация-устройства-для-magisk)
Если вы пропатчили загрузочный образ вручную через приложение Magisk (вместо автоматического идентичного патчинга через avbroot), используйте следующий аргумент:
```bash
--prepatched /путь/к/magisk_patched-xxxxx_yyyyy.img
```
* Для получения root-доступа с использованием KernelSU:
```bash
--prepatched /путь/к/kernelsu_boot.img
```
* Без root-доступа:
```bash
--rootless
```
Больше информации про существующие аргументы можно найти в разделе [расширенного использования.](#расширенное-использование)
Если название для `--output` не указывается, то готовый файл будет записан как `<название-ota-zip-в-input>.patched`.
5. Готово! Для прошивки пропатченного OTA следуйте инструкции в разделе [первоначальной настройки.](#первоначальная-настройка) Для последующих обновлений тоже есть соответствующий [раздел обновлений.](#обновления)
## Генерация ключей
Во время патчинга OTA, avbroot подписывает несколько компонентов:
* загрузочный образ (boot)
* образ vbmeta
* payload из OTA
* сам архив OTA
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
1. Сгенерируйте ключи подписи для AVB и OTA.
```bash
avbroot key generate-key -o avb.key
avbroot key generate-key -o ota.key
```
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
```bash
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
```
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
```bash
avbroot key generate-cert -k ota.key -o ota.crt
```
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке PKCS8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
Если вы потеряете ключ(-и) подписи AVB или OTA, вы больше не сможете подписывать новые OTA-архивы. Придется генерировать новые ключи подписи и разблокировать загрузчик (что приведет к стиранию всех данных). В таком случае возвращайтесь к инструкции в разделе [использования.](#использование)
## Первоначальная настройка
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
```bash
fastboot flashing unlock
```
2. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
3. Извлекаем из пропатченного OTA модифицированные образы:
```bash
avbroot ota extract \
--input /путь/к/ota.zip.patched \
--directory extracted \
--fastboot
```
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
4. Прошейте извлеченные образы разделов.
```bash
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
```
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
5. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
```bash
fastboot reboot-bootloader
fastboot erase avb_custom_key
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
```
6. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
Установите приложение Magisk или KernelSU и выполните следующую команду:
```bash
adb shell su -c 'dmesg | grep libfs_avb'
```
Если AVB работает корректно, будет выведено следующее сообщение:
```bash
init: [libfs_avb]Returning avb_handle with status: Success
```
7. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
```bash
fastboot flashing lock
```
Подтвердите нажатием клавиш уменьшения громкости и включения, а после перезагрузитесь в систему.
Напоминаю: **не отключайте `Заводскую разблокировку`!**
**ПРЕДУПРЕЖДЕНИЕ**: Если вы прошили CalyxOS, мастер настройки [автоматически отключит опцию `Заводской разблокировки`.](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140) Не забудьте снова включить её вручную в настройках для разработчиков. Для перестраховки можете использовать [модуль `OEMUnlockOnBoot`,](https://github.com/chenxiaolong/OEMUnlockOnBoot) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
8. Готово. Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
## Обновления
Обновления Android, Magisk и KernelSU выполняются одинаково – исключительно путем обновления или репатчинга того же самого OTA.
1. Если Magisk или KernelSU обновились, сначала установите их новый `.apk`. Если вы случайно открыли приложение после обновления, убедитесь, что оно не начало прошивать загрузочный образ. Если появится предложение обновить сам загрузочный образ – отклоните его.
2. Следуйте инструкции в разделе [использования,](#использование) чтобы пропатчить OTA уже с новым .apk Magisk'а/предварительно пропатченным образом с Magisk или KernelSU.
3. Перезагрузитесь в режим Recovery. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем нажмите кнопку увеличения громкости один раз.
4. Обновитесь (Apply update from adb → `adb sideload <ota.zip.patched>`).
5. Готово!
## Возврат на заводскую прошивку
Если вы хотите отказаться от использования avbroot и вернуться на стоковую прошивку:
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик. Это приведет к стиранию всех пользовательских данных.
2. Удалите пользовательский публичный ключ AVB.
```bash
fastboot erase avb_custom_key
```
3. Прошейте стоковую прошивку. Готово.
## OTA-обновления
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как при загрузке в Android, так и при сайдлоадинге через Recovery.
Рекомендуется отключить приложение обновлений системы, чтобы оно не пыталось установить непропатченные OTA:
* Стоковая прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
Если вы хотите поднять собственный сервер для ОТА-обновлений, вам может быть интересно приложение [Custota.](https://github.com/chenxiaolong/Custota)
## Режим обслуживания
Некоторые устройства поставляются с режимом обслуживания, который загружает систему с чистым образом `userdata`, благодаря чему специалист по ремонту может проводить диагностику устройства, не имея доступа к пользовательским данным владельца.
Если на устройстве есть root-права, использовать этот режим небезопасно. Если у вас обычная сборка Magisk/KernelSU, не подписанная вашим собственным ключом, кто угодно может установить официальное приложение Magisk/KernelSU в режиме обслуживания и запросто получить root-права без какой-либо аутентификации.
Потому, чтобы безопасно использовать режим обслуживания:
1. Отключите root-доступ на устройстве, пропатчив OTA с аргументом `--rootless` (вместо `--magisk` или `--prepatched`) и прошив его.
2. Включите режим обслуживания.
3. Получив отремонтированное устройство обратно, выйдите из режима обслуживания.
4. Прошейте рутированный OTA в обычном режиме.
Поскольку удаление root-прав и повторное их получение выполняются путем перепрошивки OTA, данные устройства стёрты не будут.
## Предварительная инициализация устройства для Magisk
Magisk версии 25211 и новее требует наличие раздела, доступного для записи пользовательских правил SELinux, к которым необходимо обращаться на ранних этапах загрузки. Его можно определить только на реальном устройстве, поэтому avbroot требует указания точного названия с помощью аргумента `--magisk-preinit-device <имя>`. Чтобы получить имя раздела:
1. Извлеките загрузочный образ из оригинального, непропатченного OTA:
```bash
avbroot ota extract \
--input /path/to/ota.zip \
--directory . \
--boot-only
```
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
Приложение Magisk выведет в лог строку, подобную следующей:
```
- Pre-init storage partition device ID: <имя>
```
Также и avbroot может вывести информацию о разделе, обнаруженном Magisk, для этого выполните команду:
```bash
avbroot boot magisk-info \
--image magisk_patched-*.img
```
Имя раздела будет выведено как: `PREINITDEVICE=<имя>`.
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
## Проверка OTA
Чтобы проверить все подписи и хэши, связанные с установкой OTA и процессом загрузки AVB, выполните команду:
```bash
avbroot ota verify \
--input /путь/к/ota.zip \
--cert-ota /путь/к/ota.crt \
--public-key-avb /путь/к/avb_pkmd.bin
```
Эта команда работает для любого OTA, независимо от того, пропатчено оно или нет.
Если опции `--cert-ota` и `--public-key-avb` не указаны, то подписи проверяются только на корректность, не проверяя, совпадают ли они внутри всех файлов.
## Подсказки через Tab
Поскольку avbroot имеет множество опций, будет удобно настроить подсказки с автозаполнением для используемой оболочки. Конфигурации генерируются в самом avbroot.
#### bash
Добавьте в `~/.bashrc`:
```bash
eval "$(avbroot completion -s bash)"
```
#### zsh
Добавьте в `~/.zshrc`:
```bash
eval "$(avbroot completion -s zsh)"
```
#### fish
Добавьте в `~/.config/fish/config.fish`:
```bash
avbroot completion -s fish | source
```
#### PowerShell
Добавьте в загрузочный скрипт PowerShell (`profile.ps1`):
```powershell
Invoke-Expression (& avbroot completion -s powershell)
```
## Расширенное использование
### Использование заранее пропатченного boot.img
avbroot может подменить используемый загрузочный образ на заранее пропатченный (вместо того, чтобы самостоятельно применять патч). Это пригодится в случае, если у вас уже имеется пропатченный через приложение Magisk образ ядра или образ с поддержкой KernelSU. Для этого используйте аргумент `--prepatched <загрузочный образ>` вместо `--magisk <apk>`. То есть, указав `--prepatched`, avbroot пропустит применение патчинга Magisk'ом, но по-прежнему применит патч OTA-сертификата.
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
### Пропуск патчей для root-доступа
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
### Подмена разделов
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
### Очистка флагов vbmeta
Некоторые сборки Android-прошивок могут поставляться с образом `vbmeta`, в котором флаги установлены таким образом, что AVB фактически отключен. Если avbroot сталкивается с такими образом, процесс патчинга завершается ошибкой с сообщением следующего типа:
```
Verified boot is disabled by vbmeta's header flags: 0x3
```
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
### Использование в неинтерактивном режиме
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
* Предоставить пароли через файлы:
```bash
avbroot ota patch \
--pass-avb-file /путь/к/avb.passphrase \
--pass-ota-file /путь/к/ota.passphrase \
<...>
```
На Unix-подобных системах "файлы" могут быть каналами ("pipes"). В оболочках, поддерживающих подстановку процесса (bash, zsh и т. д.), пароль можно запросить с помощью команды (например, запрашивая у менеджера паролей).
```bash
avbroot ota patch \
--pass-avb-file <(команда для запроса пароля AVB) \
--pass-ota-file <(команда для запроса пароля OTA) \
<...>
```
* Предоставить пароли через переменные среды. Это менее безопасно, поскольку любой процесс, запущенный от имени того же пользователя, может видеть значения переменных среды.
```bash
export PASSPHRASE_AVB="пароль AVB"
export PASSPHRASE_OTA="пароль OTA"
avbroot ota patch \
--pass-avb-env-var PASSPHRASE_AVB \
--pass-ota-env-var PASSPHRASE_OTA \
<...>
```
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
### Извлечение всей OTA
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
```bash
avbroot ota extract \
--input /путь/к/ota.zip \
--directory extracted \
--all
```
## Сборка из исходного кода
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
```bash
cargo build --release
```
Исполняемый файл будет записан в `target/release/avbroot`.
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
## Проверка цифровых подписей
Сначала сохраните открытый ключ в файл, перечисляющий ключи, которым можно доверять. Это тот же ключ, который указан [в профиле автора.](https://github.com/chenxiaolong/)
```bash
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
```
Затем проверьте подпись zip-файла, используя список доверенных ключей.
```bash
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
```
Если файл успешно проверен, вывод будет следующим:
```
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
```
## Вклад
Буду рад вашему вкладу в разработку! Однако я вряд ли приму изменения для поддержки устройств, которые ведут себя значительно иначе, чем устройства Pixel.
## Лицензия
avbroot распространяется по лицензии GPLv3. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
+14 -21
View File
@@ -10,12 +10,12 @@ publish = false
[dependencies]
anyhow = "1.0.75"
base64 = "0.21.3"
base64 = "0.22.1"
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"] }
@@ -24,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.3.0"
lz4_flex = "0.11.1"
memchr = "2.6.0"
num-bigint-dig = "0.8.4"
@@ -46,26 +47,18 @@ sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "1.0.47"
toml_edit = { version = "0.21.0", features = ["serde"] }
toml_edit = { version = "0.22.9", 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
# destructor if the decoder is fed invalid data. While this never happens during
# normal operation, it is possible to run into this by running `ota extract`
# against a `--stripped` OTA file.
# https://github.com/alexcrichton/bzip2-rs/pull/99
# There are multiple upstream bugs that cause infinite loops in the Drop
# implementation of write::BzDecoder. Unfortunately, the project is no longer
# maintained, so we have to maintain our own fork with the necessary fixes.
[dependencies.bzip2]
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"
git = "https://github.com/chenxiaolong/bzip2-rs"
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
@@ -79,10 +72,10 @@ rustix = { version = "0.38.9", default-features = false, features = ["process"]
[build-dependencies]
prost-build = "0.12.1"
protox = "0.5.0"
protox = "0.6.0"
[dev-dependencies]
assert_matches = "1.5.0"
[features]
static = ["bzip2/static", "xz2/static"]
static = ["bzip2/static", "liblzma/static"]
+118 -3
View File
@@ -3,10 +3,17 @@
* 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, hashtree, key, ota};
@@ -29,16 +36,124 @@ 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),
+65 -36
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -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,
@@ -57,16 +57,22 @@ fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
Ok((info, reader))
}
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo) -> Result<()> {
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
let mut writer = BufWriter::new(file);
if let Some(f) = &mut info.footer {
avb::write_appended_image(&mut writer, &info.header, f, info.image_size)
.context("Failed to write appended AVB image")?;
info.image_size = if let Some(f) = &mut info.footer {
let image_size = if recompute_size {
None
} else {
Some(info.image_size)
};
avb::write_appended_image(&mut writer, &info.header, f, image_size)
.context("Failed to write appended AVB image")?
} else {
avb::write_root_image(&mut writer, &info.header, 4096)
.context("Failed to write root AVB image")?;
}
.context("Failed to write root AVB image")?
};
writer.flush().context("Failed to flush writes")?;
@@ -94,15 +100,13 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
/// Packing with insecure algorithms is intentionally not supported, so promote
/// to a secure algorithm if needed.
fn promote_insecure_hash_algorithm(algorithm: &str) -> &str {
fn promote_insecure_hash_algorithm(algorithm: &mut String) {
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
const NEW_ALGORITHM: &str = "sha256";
if INSECURE_ALGORITHMS.contains(&algorithm) {
warning!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
NEW_ALGORITHM
} else {
algorithm
if INSECURE_ALGORITHMS.contains(&algorithm.as_str()) {
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
NEW_ALGORITHM.clone_into(algorithm);
}
}
@@ -166,7 +170,7 @@ fn write_raw_and_verify(
if let Err(e) = result {
if ignore_invalid {
warning!("{e:?}");
warn!("{e:?}");
} else {
return Err(e);
}
@@ -193,13 +197,13 @@ fn write_raw_and_update(
match info.header.appended_descriptor_mut()? {
AppendedDescriptorMut::HashTree(d) => {
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.image_size = image_size;
d.update(&raw_file, &raw_file, None, cancel_signal)
.context("Failed to update hash tree descriptor")?;
}
AppendedDescriptorMut::Hash(d) => {
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.image_size = image_size;
raw_file.rewind()?;
d.update(&mut raw_file, cancel_signal)
@@ -358,16 +362,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 +393,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 +455,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 +506,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,18 +514,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::HashTree(_)) if repair => {
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
warning!("Attempting to repair using FEC data{suffix}");
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,
@@ -528,7 +533,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)
@@ -547,9 +552,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))
@@ -560,7 +569,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) => {
@@ -622,12 +631,16 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info)?;
write_avb_image(file, &mut info, cli.recompute_size)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
display_info(&cli.display, &info);
if let Some(path) = &cli.output_info {
write_info(path, &info)?;
}
Ok(())
}
@@ -641,7 +654,7 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
// Write new hash tree and FEC data instead of copying the original.
// 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();
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.update(&file, &file, None, cancel_signal)?;
}
@@ -656,7 +669,7 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info)?;
write_avb_image(file, &mut info, false)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
@@ -706,7 +719,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(())
}
@@ -824,12 +837,28 @@ struct PackCli {
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
input_info: PathBuf,
/// Path to output AVB info TOML.
///
/// If specified, the AVB info containing all recomputed fields will be
/// written to this file. This can point to the same file as --input-info.
#[arg(long, value_name = "FILE", value_parser)]
output_info: Option<PathBuf>,
/// Path to input raw image.
///
/// Appended AVB images require a raw image.
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
input_raw: PathBuf,
/// Recompute image size.
///
/// By default, it is assumed that the image has a fixed size specified by
/// the image_size top-level field in the AVB info TOML. If flag is passed,
/// then that field is ignored and the smallest possible output image will
/// be created.
#[arg(long)]
recompute_size: bool,
#[command(flatten)]
key: KeyGroup,
-15
View File
@@ -12,18 +12,3 @@ 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;
+227 -82
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -10,10 +10,10 @@ use std::{
fmt::Display,
fs::{self, File},
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
mem,
ops::Range,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Mutex},
time::Instant,
};
use anyhow::{anyhow, bail, Context, Result};
@@ -24,21 +24,24 @@ use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
use rsa::RsaPrivateKey;
use tempfile::NamedTempFile;
use topological_sort::TopologicalSort;
use tracing::{debug_span, info, warn};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use crate::{
cli::{self, status, warning},
cli,
crypto::{self, PassphraseSource},
format::{
avb::Header,
avb::{self, Descriptor},
avb::{self, Descriptor, Header},
ota::{self, SigningWriter, ZipEntry},
padding,
payload::{self, PayloadHeader, PayloadWriter},
},
patch::{
boot::{self, BootImagePatch, MagiskRootPatcher, OtaCertPatcher, PrepatchedImagePatcher},
boot::{
self, BootImagePatch, DsuPubKeyPatcher, MagiskRootPatcher, OtaCertPatcher,
PrepatchedImagePatcher,
},
system,
},
protobuf::{
@@ -149,8 +152,10 @@ fn open_input_files(
.collect::<HashSet<_>>();
for name in all_images {
let _span = debug_span!("image", name).entered();
if let Some(path) = external_images.get(name) {
status!("Opening external image: {name}: {path:?}");
info!("Opening external image: {name}: {path:?}");
let file = File::open(path)
.map(PSeekFile::new)
@@ -163,7 +168,7 @@ fn open_input_files(
},
);
} else {
status!("Extracting from original payload: {name}");
info!("Extracting from original payload: {name}");
let file = tempfile::tempfile()
.map(PSeekFile::new)
@@ -185,29 +190,20 @@ fn open_input_files(
}
/// Patch the boot images listed in `required_images`. Not every image is
/// necessarily patched. An [`OtaCertPatcher`] is always applied to the boot
/// image that contains the trusted OTA certificate list. If `root_patcher` is
/// specified, then it is used to patch the boot image for root access. If the
/// original image is signed, then it will be re-signed with `key_avb`.
/// necessarily patched. Each patcher will determine which image it should
/// target. If the original image is signed, then it will be re-signed with
/// `key_avb`.
fn patch_boot_images<'a, 'b: 'a>(
required_images: &'b RequiredImages,
input_files: &mut HashMap<String, InputFile>,
root_patcher: Option<Box<dyn BootImagePatch + Sync>>,
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
key_avb: &RsaPrivateKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<()> {
let input_files = Mutex::new(input_files);
let mut boot_patchers = Vec::<Box<dyn BootImagePatch + Sync>>::new();
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
if let Some(p) = root_patcher {
boot_patchers.push(p);
}
let boot_partitions = required_images.iter_boot().collect::<Vec<_>>();
status!(
info!(
"Patching boot images: {}",
joined(sorted(boot_partitions.iter())),
);
@@ -252,7 +248,9 @@ fn patch_system_image<'a, 'b: 'a>(
bail!("No system partition found");
};
status!("Patching system image: {target}");
let _span = debug_span!("image", name = target).entered();
info!("Patching system image: {target}");
let input_file = input_files.get_mut(target).unwrap();
@@ -280,7 +278,7 @@ fn patch_system_image<'a, 'b: 'a>(
input_file.state = InputFileState::Modified;
status!("Patched otacerts.zip offsets in {target}: {ranges:?}");
info!("Patched otacerts.zip offsets in {target}: {ranges:?}");
ranges.extend(other_ranges);
@@ -384,7 +382,7 @@ fn get_vbmeta_patch_order(
}
if !missing.is_empty() {
warning!("Partitions aren't protected by AVB: {:?}", joined(missing));
warn!("Partitions aren't protected by AVB: {}", joined(missing));
}
// Ensure that there's only a single root of trust. Otherwise, there could
@@ -475,7 +473,7 @@ fn update_security_descriptors(
// vbmeta is signed; Use a chain descriptor.
match parent_descriptor {
Descriptor::ChainPartition(pd) => {
pd.public_key = child_header.public_key.clone();
child_header.public_key.clone_into(&mut pd.public_key);
}
_ => {
bail!("{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor");
@@ -489,9 +487,7 @@ fn update_security_descriptors(
/// Get the text before the first equal sign in the kernel command line if it is
/// not empty.
fn cmdline_prefix(cmdline: &str) -> Option<&str> {
let Some((prefix, _)) = cmdline.split_once('=') else {
return None;
};
let (prefix, _) = cmdline.split_once('=')?;
if prefix.is_empty() {
return None;
}
@@ -520,7 +516,7 @@ fn update_metadata_descriptors(parent_header: &mut Header, child_header: &Header
});
if let Some(pd) = parent_property {
pd.value = cd.value.clone();
cd.value.clone_into(&mut pd.value);
} else {
parent_header
.descriptors
@@ -540,7 +536,7 @@ fn update_metadata_descriptors(parent_header: &mut Header, child_header: &Header
});
if let Some(pd) = parent_property {
pd.cmdline = cd.cmdline.clone();
cd.cmdline.clone_into(&mut pd.cmdline);
} else {
parent_header
.descriptors
@@ -635,6 +631,8 @@ fn compress_image(
ranges: Option<&[Range<u64>]>,
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<usize>>> {
let _span = debug_span!("image", name).entered();
file.rewind()?;
let writer = tempfile::tempfile()
@@ -650,7 +648,7 @@ fn compress_image(
.unwrap();
if let Some(r) = ranges {
status!("Compressing partial image: {name}: {r:?}");
info!("Compressing partial image: {name}: {r:?}");
match payload::compress_modified_image(
&*file,
@@ -668,13 +666,13 @@ fn compress_image(
// If we can't take advantage of the optimization, we can still
// compress the whole image.
Err(payload::Error::ExtentsNotInOrder) => {
warning!("Cannot use optimization for {name}: extents not in order");
warn!("Cannot use optimization for {name}: extents not in order");
}
Err(e) => return Err(e.into()),
}
}
status!("Compressing full image: {name}");
info!("Compressing full image: {name}");
// Otherwise, compress the entire image.
let (partition_info, operations) =
@@ -694,7 +692,7 @@ fn patch_ota_payload(
payload: &(dyn ReadSeekReopen + Sync),
writer: impl Write,
external_images: &HashMap<String, PathBuf>,
root_patcher: Option<Box<dyn BootImagePatch + Sync>>,
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
clear_vbmeta_flags: bool,
key_avb: &RsaPrivateKey,
key_ota: &RsaPrivateKey,
@@ -746,9 +744,8 @@ fn patch_ota_payload(
patch_boot_images(
&required_images,
&mut input_files,
root_patcher,
boot_patchers,
key_avb,
cert_ota,
cancel_signal,
)?;
@@ -771,7 +768,7 @@ fn patch_ota_payload(
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_files, &vbmeta_headers)?;
status!(
info!(
"Patching vbmeta images: {}",
joined(vbmeta_order.iter().map(|(n, _)| n)),
);
@@ -810,7 +807,7 @@ fn patch_ota_payload(
})
.collect::<Result<HashMap<_, _>>>()?;
status!("Generating new OTA payload");
info!("Generating new OTA payload");
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
.context("Failed to write payload header")?;
@@ -889,7 +886,7 @@ fn patch_ota_zip(
zip_reader: &mut ZipArchive<impl Read + Seek>,
mut zip_writer: &mut ZipWriter<impl Write>,
external_images: &HashMap<String, PathBuf>,
mut root_patch: Option<Box<dyn BootImagePatch + Sync>>,
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
clear_vbmeta_flags: bool,
key_avb: &RsaPrivateKey,
key_ota: &RsaPrivateKey,
@@ -910,7 +907,7 @@ fn patch_ota_zip(
}
if !missing.is_empty() {
bail!("Missing entries in OTA zip: {:?}", joined(missing));
bail!("Missing entries in OTA zip: {}", joined(missing));
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
bail!(
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
@@ -926,6 +923,8 @@ fn patch_ota_zip(
let mut last_entry_used_zip64 = false;
for path in &paths {
let _span = debug_span!("zip", entry = path).entered();
let mut reader = zip_reader
.by_name(path)
.with_context(|| format!("Failed to open zip entry: {path}"))?;
@@ -984,13 +983,13 @@ fn patch_ota_zip(
match path.as_str() {
ota::PATH_OTACERT => {
// Use the user's certificate
status!("Replacing zip entry: {path}");
info!("Replacing zip entry: {path}");
crypto::write_pem_cert(&mut writer, cert_ota)
.with_context(|| format!("Failed to write entry: {path}"))?;
}
ota::PATH_PAYLOAD => {
status!("Patching zip entry: {path}");
info!("Patching zip entry: {path}");
if reader.compression() != CompressionMethod::Stored {
bail!("{path} is not stored uncompressed");
@@ -1009,7 +1008,7 @@ fn patch_ota_zip(
&mut writer,
external_images,
// There's only one payload in the OTA.
root_patch.take(),
mem::take(&mut boot_patchers),
clear_vbmeta_flags,
key_avb,
key_ota,
@@ -1022,7 +1021,7 @@ fn patch_ota_zip(
payload_metadata_size = Some(m);
}
ota::PATH_PROPERTIES => {
status!("Patching zip entry: {path}");
info!("Patching zip entry: {path}");
// payload.bin is guaranteed to be patched first.
writer
@@ -1030,7 +1029,7 @@ fn patch_ota_zip(
.with_context(|| format!("Failed to write payload properties: {path}"))?;
}
_ => {
status!("Copying zip entry: {path}");
info!("Copying zip entry: {path}");
stream::copy(&mut reader, &mut writer, cancel_signal)
.with_context(|| format!("Failed to copy zip entry: {path}"))?;
@@ -1049,7 +1048,7 @@ fn patch_ota_zip(
last_entry_used_zip64 = use_zip64;
}
status!("Generating new OTA metadata");
info!("Generating new OTA metadata");
let data_descriptor_size = if last_entry_used_zip64 { 24 } else { 16 };
let metadata = ota::add_metadata(
@@ -1080,7 +1079,7 @@ fn extract_ota_zip(
}
}
status!("Extracting from the payload: {}", joined(images));
info!("Extracting from the payload: {}", joined(images));
// Pre-open all output files.
let output_files = images
@@ -1113,6 +1112,8 @@ fn extract_ota_zip(
)
.context("Failed to extract images from payload")?;
info!("Successfully extracted OTA");
Ok(())
}
@@ -1166,7 +1167,7 @@ fn verify_partition_hashes(
pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()> {
if cli.boot_partition.is_some() {
warning!("Ignoring --boot-partition: deprecated and no longer needed");
warn!("Ignoring --boot-partition: deprecated and no longer needed");
}
let output = cli.output.as_ref().map_or_else(
@@ -1215,35 +1216,31 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
external_images.insert(name.to_owned(), path.to_owned());
}
let root_patcher = if let Some(magisk) = &cli.root.magisk {
let patcher: Box<dyn BootImagePatch + Sync> = Box::new(
let mut boot_patchers = Vec::<Box<dyn BootImagePatch + Sync>>::new();
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
if let Some(magisk) = &cli.root.magisk {
boot_patchers.push(Box::new(
MagiskRootPatcher::new(
magisk,
cli.magisk_preinit_device.as_deref(),
cli.magisk_random_seed,
cli.ignore_magisk_warnings,
move |s| warning!("{s}"),
)
.context("Failed to create Magisk boot image patcher")?,
);
Some(patcher)
));
} else if let Some(prepatched) = &cli.root.prepatched {
let patcher: Box<dyn BootImagePatch + Sync> = Box::new(PrepatchedImagePatcher::new(
boot_patchers.push(Box::new(PrepatchedImagePatcher::new(
prepatched,
cli.ignore_prepatched_compat + 1,
move |s| {
warning!("{s}");
},
));
Some(patcher)
)));
} else {
assert!(cli.root.rootless);
None
};
let start = Instant::now();
if cli.dsu {
boot_patchers.push(Box::new(DsuPubKeyPatcher::new(key_avb.to_public_key())));
}
let raw_reader = File::open(&cli.input)
.map(PSeekFile::new)
@@ -1270,7 +1267,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
&mut zip_reader,
&mut zip_writer,
&external_images,
root_patcher,
boot_patchers,
cli.clear_vbmeta_flags,
&key_avb,
&key_ota,
@@ -1292,7 +1289,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
temp_writer.flush().context("Failed to flush output zip")?;
// We do a lot of low-level hackery. Reopen and verify offsets.
status!("Verifying metadata offsets");
info!("Verifying metadata offsets");
temp_writer.rewind().context("Failed to seek output zip")?;
ota::verify_metadata(
BufReader::new(&mut temp_writer),
@@ -1301,7 +1298,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
)
.context("Failed to verify OTA metadata offsets")?;
status!("Completed after {:.1}s", start.elapsed().as_secs_f64());
info!("Successfully patched OTA");
// NamedTempFile forces 600 permissions on temp files because it's the safe
// option for a shared /tmp. Since we're writing to the output file's
@@ -1334,7 +1331,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Result<()> {
if cli.boot_partition.is_some() {
warning!("Ignoring --boot-partition: deprecated and no longer needed");
warn!("Ignoring --boot-partition: deprecated and no longer needed");
}
let raw_reader = File::open(&cli.input)
@@ -1342,11 +1339,12 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
let payload_entry = zip
.by_name(ota::PATH_PAYLOAD)
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
let payload_offset = payload_entry.data_start();
let payload_size = payload_entry.size();
let (payload_offset, payload_size) = {
let entry = zip
.by_name(ota::PATH_PAYLOAD)
.with_context(|| format!("Failed to open zip entry: {}", ota::PATH_PAYLOAD))?;
(entry.data_start(), entry.size())
};
// Open the payload data directly.
let mut payload_reader = SectionReader::new(
@@ -1399,6 +1397,144 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
cancel_signal,
)?;
if cli.fastboot {
const ANDROID_INFO: &str = "android-info.txt";
const FASTBOOT_INFO: &str = "fastboot-info.txt";
// Generate android-info.txt, which is always required for fastboot's
// flashall subcommand. We only add a basic device check to avoid
// accidental flashes on the wrong device.
let mut metadata_entry = zip
.by_name(ota::PATH_METADATA_PB)
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_METADATA_PB))?;
let mut metadata_raw = vec![];
metadata_entry
.read_to_end(&mut metadata_raw)
.with_context(|| format!("Failed to read OTA metadata: {}", ota::PATH_METADATA_PB))?;
let metadata = ota::parse_protobuf_metadata(&metadata_raw)
.with_context(|| format!("Failed to parse OTA metadata: {}", ota::PATH_METADATA_PB))?;
let device = metadata
.precondition
.as_ref()
.and_then(|p| p.device.first())
.ok_or_else(|| anyhow!("Device codename not found in OTA metadata"))?;
directory
.write(ANDROID_INFO, format!("require board={device}\n"))
.with_context(|| format!("Failed to write file: {ANDROID_INFO}"))?;
// Find out which images can be flashed with fastboot. The bootloader
// (and potentially modem) partitions need to be flashed as a whole and
// an OTA doesn't contain sufficient information to generate the
// required combined file.
let mut flashable_images = BTreeSet::new();
for name in &unique_images {
let file = directory
.open(format!("{name}.img"))
.with_context(|| format!("Failed to open image for reading: {name}"))?;
match avb::load_image(file) {
Ok(_) => {
flashable_images.insert(name);
}
// Treat images without AVB metadata as bootloader partitions.
Err(avb::Error::InvalidHeaderMagic(_)) => continue,
Err(e) => return Err(e).with_context(|| format!("Failed to load image: {name}")),
}
}
// Generate fastboot-info.txt to be able to control how exactly the
// images are flashed. This solves two problems:
//
// 1. fastboot flashall, by default, expects super_empty.img to exist
// for A/B devices. If it doesn't exist, it assumes that all images
// are meant for non-dynamic partitions and skips the fastbootd
// reboot. We cannot generate a proper super_empty.img because it
// requires knowing the number of super partitions along with their
// names and sizes. This information is not available in an OTA. The
// fastboot info file allows us to control when to reboot to
// fastbootd and flash each partition.
//
// This approach is a bit slower because we have to reboot into
// fastbootd. When the device only has a single super partition,
// fastboot will normally locally combine super_empty.img with the
// individual partitions to form a full super image and then flash it
// via the bootloader's fastboot mode.
//
// Also, because we can't generate super_empty.img, if the super
// partition metadata on the device somehow becomes corrupted,
// running fastboot flash all using avbroot's extracted images isn't
// sufficient for recovering the device. The user will need to copy
// that file from the factory image into the output directory and add
// an `update-super` line after `reboot fastboot` the fastboot info
// file.
//
// 2. fastboot flashall does not look at all .img files in a directory.
// Instead, it has a hardcoded list of partitions known to AOSP. This
// means obscure OEM-specific partitions are just silently ignored.
// Using a fastboot info file with explicit flash instructions avoids
// this problem entirely.
let mut dynamic = BTreeSet::new();
if let Some(dpm) = &header.manifest.dynamic_partition_metadata {
for group in &dpm.groups {
for name in &group.partition_names {
if unique_images.contains(name) {
dynamic.insert(name);
}
}
}
}
let flash_command = |name| {
let (prefix, suffix) = if flashable_images.contains(name) {
("", "")
} else {
("#", " # Bootloader/modem images cannot be flashed")
};
let extra_arg = if *name == "vbmeta" {
"--apply-vbmeta "
} else {
""
};
format!("{prefix}flash {extra_arg}{name}{suffix}\n")
};
let mut fastboot_info = String::new();
fastboot_info.push_str("# Generated by avbroot\n");
fastboot_info.push_str("version 1\n");
fastboot_info.push_str("# Flash non-dynamic partitions\n");
for name in &unique_images {
if !dynamic.contains(name) {
fastboot_info.push_str(&flash_command(name));
}
}
fastboot_info.push_str("# Reboot to fastbootd\n");
fastboot_info.push_str("reboot fastboot\n");
fastboot_info.push_str("# Flash dynamic partitions\n");
for name in &dynamic {
fastboot_info.push_str(&flash_command(name));
}
fastboot_info.push_str("# Wipe data when -w flag is used\n");
fastboot_info.push_str("if-wipe erase userdata\n");
fastboot_info.push_str("if-wipe erase metadata\n");
directory
.write(FASTBOOT_INFO, fastboot_info)
.with_context(|| format!("Failed to write file: {FASTBOOT_INFO}"))?;
}
Ok(())
}
@@ -1408,7 +1544,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
let mut reader = BufReader::new(raw_reader);
status!("Verifying whole-file signature");
info!("Verifying whole-file signature");
let embedded_cert = ota::verify_ota(&mut reader, cancel_signal)?;
@@ -1426,13 +1562,13 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
bail!("OTA has a valid signature, but was not signed with: {p:?}");
}
} else {
warning!("Whole-file signature is valid, but its trust is unknown");
warn!("Whole-file signature is valid, but its trust is unknown");
}
ota::verify_metadata(&mut reader, &metadata, header.blob_offset)
.context("Failed to verify OTA metadata offsets")?;
status!("Verifying payload");
info!("Verifying payload");
let pfs_raw = metadata
.property_files
@@ -1450,7 +1586,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
payload::verify_payload(section_reader, &ota_cert, &properties, cancel_signal)?;
status!("Extracting partition images to temporary directory");
info!("Extracting partition images to temporary directory");
let authority = ambient_authority();
let temp_dir = TempDir::new(authority).context("Failed to create temporary directory")?;
@@ -1473,11 +1609,11 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
cancel_signal,
)?;
status!("Verifying partition hashes");
info!("Verifying partition hashes");
verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal)?;
status!("Checking ramdisk's otacerts.zip");
info!("Checking ramdisk's otacerts.zip");
{
let required_images = RequiredImages::new(&header.manifest);
@@ -1509,7 +1645,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
}
}
status!("Verifying AVB signatures");
info!("Verifying AVB signatures");
let public_key = if let Some(p) = &cli.public_key_avb {
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
@@ -1533,7 +1669,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
)?;
cli::avb::verify_descriptors(&temp_dir, &descriptors, false, cancel_signal)?;
status!("Signatures are all valid!");
info!("Signatures are all valid!");
Ok(())
}
@@ -1700,6 +1836,10 @@ pub struct PatchCli {
)]
pub ignore_prepatched_compat: u8,
/// Add AVB public key to trusted keys for DSU.
#[arg(long, help_heading = HEADING_OTHER)]
pub dsu: bool,
/// Forcibly clear vbmeta flags if they disable AVB.
#[arg(long, help_heading = HEADING_OTHER)]
pub clear_vbmeta_flags: bool,
@@ -1708,6 +1848,7 @@ pub struct PatchCli {
#[arg(
long,
value_name = "PARTITION",
hide = true,
help_heading = HEADING_OTHER
)]
pub boot_partition: Option<String>,
@@ -1733,8 +1874,12 @@ pub struct ExtractCli {
pub boot_only: bool,
/// (Deprecated: no longer needed)
#[arg(long, value_name = "PARTITION")]
#[arg(long, value_name = "PARTITION", hide = true)]
pub boot_partition: Option<String>,
/// Generate fastboot info files.
#[arg(long)]
pub fastboot: bool,
}
/// Verify signatures of an OTA.
+6 -3
View File
@@ -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 {
+112 -95
View File
@@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
cmp, fmt,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
str,
@@ -29,8 +29,8 @@ use crate::{
padding,
},
stream::{
self, CountingReader, FromReader, ReadDiscardExt, ReadSeekReopen, ReadStringExt, ToWriter,
WriteSeekReopen, WriteStringExt, WriteZerosExt,
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ReadSeekReopen,
ReadStringExt, ToWriter, WriteSeekReopen, WriteStringExt, WriteZerosExt,
},
util,
};
@@ -50,13 +50,30 @@ pub const FOOTER_MAGIC: [u8; 4] = *b"AVBf";
/// for early fail. No individual field can actually be this size.
pub const HEADER_MAX_SIZE: u64 = 64 * 1024;
/// Maximum hash tree size. The current limit equals the hash tree size for a
/// 4GiB image using SHA512 digests and a block size of 4096.
pub const HASH_TREE_MAX_SIZE: u64 = 68_177_920;
/// Maximum hash tree size. The current limit equals the hash tree size for an
/// 8GiB image using SHA256 digests and a block size of 4096. This is equal to:
///
/// ```rust
/// use avbroot::format::hashtree::HashTree;
/// let size = HashTree::new(4096, &ring::digest::SHA256, b"")
/// .compute_level_offsets(8 * 1024 * 1024 * 1024)
/// .unwrap()
/// .first()
/// .map(|r| r.end)
/// .unwrap_or(0);
/// ```
pub const HASH_TREE_MAX_SIZE: u64 = 67_637_248;
/// Maximum FEC data size. The current limit equals the FEC data size for a 4GiB
/// image using 2 parity bytes per codeword.
pub const FEC_DATA_MAX_SIZE: u64 = 33_959_936;
/// Maximum FEC data size. The current limit equals the FEC data size for an
/// 8GiB image using 2 parity bytes per codeword. This is equal to:
///
/// ```rust
/// use avbroot::format::fec::Fec;
/// let size = Fec::new(8 * 1024 * 1024 * 1024, 4096, 2)
/// .unwrap()
/// .fec_size();
/// ```
pub const FEC_DATA_MAX_SIZE: u64 = 67_911_680;
#[derive(Debug, Error)]
pub enum Error {
@@ -107,8 +124,10 @@ pub enum Error {
RsaSign(#[source] rsa::Error),
#[error("Failed to RSA verify signature")]
RsaVerify(#[source] rsa::Error),
#[error("{0} byte image size is too small to fit header or footer")]
ImageSizeTooSmall(u64),
#[error("{0} byte image size is too small to fit header")]
TooSmallForHeader(u64),
#[error("{0} byte image size is too small to fit footer")]
TooSmallForFooter(u64),
#[error("Hash tree error")]
HashTree(#[from] hashtree::Error),
#[error("FEC error")]
@@ -1834,97 +1853,95 @@ pub fn load_image(mut reader: impl Read + Seek) -> Result<(Header, Option<Footer
Ok((header, footer, image_size))
}
/// Write a vbmeta header to the specified writer. If a footer is specified, it
/// will be used as the basis of the newly written footer, with the original
/// image size, vbmeta header offset, and vbmeta header size fields updated
/// appropriately.
///
/// The writer must not have an existing vbmeta header or footer.
fn write_image_internal(
mut writer: impl Write + Seek,
header: &Header,
footer: Option<&mut Footer>,
image_size: Option<u64>,
block_size: u64,
) -> Result<()> {
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 {
let padding_size = padding::write_zeros(&mut writer, block_size)?;
eof_image_size
.checked_add(padding_size)
.ok_or_else(|| Error::FieldOutOfBounds("vbmeta_offset"))?
} else {
eof_image_size
};
header.to_writer(&mut writer)?;
let vbmeta_end = writer.stream_position()?;
if let Some(s) = image_size {
let footer_space = if footer.is_some() {
cmp::max(block_size, Footer::SIZE as u64)
} else {
0
};
if s < footer_space || vbmeta_end > s - footer_space {
return Err(Error::ImageSizeTooSmall(s));
}
}
if block_size > 0 {
padding::write_zeros(&mut writer, block_size)?;
}
if let Some(f) = footer {
let footer_offset = image_size.unwrap() - Footer::SIZE as u64;
writer.seek(SeekFrom::Start(footer_offset))?;
let original_image_size = match header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => d.image_size,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
f.original_image_size = original_image_size;
f.vbmeta_offset = vbmeta_offset;
f.vbmeta_size = vbmeta_end - vbmeta_offset;
f.to_writer(&mut writer)?;
}
Ok(())
}
/// Write a vbmeta header to the specified writer. This is meant for writing
/// vbmeta partition images, not appended vbmeta images. The writer must refer
/// to an empty file.
pub fn write_root_image(writer: impl Write + Seek, header: &Header, block_size: u64) -> Result<()> {
write_image_internal(writer, header, None, None, block_size)
/// to an empty file. Returns the size of the new file.
pub fn write_root_image(writer: impl Write, header: &Header, block_size: u64) -> Result<u64> {
let mut counting_writer = CountingWriter::new(writer);
header.to_writer(&mut counting_writer)?;
padding::write_zeros(&mut counting_writer, block_size)?;
Ok(counting_writer.stream_position()?)
}
/// Write a vbmeta header and footer to the specified writer. This is meant for
/// appending vbmeta data to existing partition data, not writing vbmeta images.
/// If `image_size` is specified, then the writer is guaranteed to not grow
/// past that size and an error is returned if the header and footer won't fit.
/// Otherwise, the writer will grow to the necessary size. Returns the size of
/// the new file.
pub fn write_appended_image(
writer: impl Write + Seek,
mut writer: impl Write + Seek,
header: &Header,
footer: &mut Footer,
image_size: u64,
) -> Result<()> {
image_size: Option<u64>,
) -> Result<u64> {
// avbtool hardcodes a 4096 block size for appended non-sparse images.
write_image_internal(writer, header, Some(footer), Some(image_size), 4096)
const BLOCK_SIZE: u64 = 4096;
// Logical image size, excluding the AVB header and footer.
let logical_image_size = 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("logical_image_size"))?,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
writer.seek(SeekFrom::Start(logical_image_size))?;
// The header start offset must be block aligned.
let header_offset = {
let padding_size = padding::write_zeros(&mut writer, BLOCK_SIZE)?;
logical_image_size
.checked_add(padding_size)
.ok_or_else(|| Error::FieldOutOfBounds("header_offset"))?
};
// The header lives at the beginning of the empty space.
let mut header_buf = Cursor::new(Vec::new());
header.to_writer(&mut header_buf)?;
let header_size = header_buf.stream_position()?;
let header_padding = padding::write_zeros(&mut header_buf, BLOCK_SIZE)?;
let header_end_padded = header_offset
.checked_add(header_size)
.and_then(|s| s.checked_add(header_padding))
.ok_or_else(|| Error::FieldOutOfBounds("header_end_padded"))?;
if let Some(s) = image_size {
if header_end_padded > s {
return Err(Error::TooSmallForHeader(s));
}
}
writer.write_all(&header_buf.into_inner())?;
// The footer lives in its own separate block at the end of the empty space.
let footer_end = if let Some(s) = image_size {
if s - header_end_padded < BLOCK_SIZE {
return Err(Error::TooSmallForFooter(s));
}
s
} else {
header_end_padded
.checked_add(BLOCK_SIZE)
.ok_or_else(|| Error::FieldOutOfBounds("footer_end"))?
};
let footer_offset = footer_end - Footer::SIZE as u64;
writer.seek(SeekFrom::Start(footer_offset))?;
footer.original_image_size = match header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => d.image_size,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
footer.vbmeta_offset = header_offset;
footer.vbmeta_size = header_size;
footer.to_writer(&mut writer)?;
Ok(footer_end)
}
+5 -5
View File
@@ -7,14 +7,14 @@ use std::io::{self, Read, Seek, Write};
use byteorder::{LittleEndian, WriteBytesExt};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use lz4_flex::frame::FrameDecoder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use xz2::{
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";
@@ -25,7 +25,7 @@ pub enum Error {
#[error("Unknown compression format")]
UnknownFormat,
#[error("XZ stream error")]
XzStream(#[from] xz2::stream::Error),
XzStream(#[from] liblzma::stream::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
+6 -6
View File
@@ -166,8 +166,8 @@ impl Fec {
return Err(Error::UnsupportedParity(parity));
}
let blocks = util::div_ceil(file_size, u64::from(block_size));
let rounds = util::div_ceil(blocks, u64::from(rs_k));
let blocks = file_size.div_ceil(u64::from(block_size));
let rounds = blocks.div_ceil(u64::from(rs_k));
// Check upfront so we don't need to do checked multiplication later.
rounds
@@ -196,7 +196,7 @@ impl Fec {
/// Get the size of the FEC data needed to cover the entire file.
#[inline]
fn fec_size(&self) -> usize {
pub fn fec_size(&self) -> usize {
usize::from(self.parity()) * self.rounds as usize * self.block_size as usize
}
@@ -225,7 +225,7 @@ impl Fec {
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
util::div_ceil(range.end, block_size)
range.end.div_ceil(block_size)
};
for block in start_block..end_block {
@@ -807,12 +807,12 @@ mod tests {
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..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(&[0..1]).unwrap(), HashSet::from([0]));
assert_eq!(
fec.rounds_for_ranges(&[4095..4096]).unwrap(),
HashSet::from([0]),
+29 -27
View File
@@ -52,18 +52,19 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
pub struct HashTree<'a> {
pub struct HashTree {
block_size: u32,
algorithm: &'static Algorithm,
salt: &'a [u8],
salted_context: Context,
}
impl<'a> HashTree<'a> {
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &'a [u8]) -> Self {
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,
algorithm,
salt,
salted_context,
}
}
@@ -71,13 +72,14 @@ impl<'a> HashTree<'a> {
/// 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 digest_size = self.algorithm.output_len().next_power_of_two();
pub 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));
let blocks = level_size.div_ceil(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)))
@@ -122,7 +124,7 @@ impl<'a> HashTree<'a> {
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
util::div_ceil(range.end, block_size)
range.end.div_ceil(block_size)
};
result.push(start_block..end_block);
@@ -143,8 +145,8 @@ impl<'a> HashTree<'a> {
cancel_signal: &AtomicBool,
) -> io::Result<()> {
// Each digest must be a power of 2.
let digest_padding =
self.algorithm.output_len().next_power_of_two() - self.algorithm.output_len();
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 {
@@ -157,8 +159,7 @@ impl<'a> HashTree<'a> {
// with padding.
buf[n..].fill(0);
let mut context = Context::new(self.algorithm);
context.update(self.salt);
let mut context = self.salted_context.clone();
context.update(&buf);
// Add the digest to the tree level. Each tree node must be a power
@@ -191,7 +192,8 @@ impl<'a> HashTree<'a> {
);
// Parallelize in larger chunks to avoid too much seek thrashing.
let digest_size = self.algorithm.output_len().next_power_of_two();
let algorithm = self.salted_context.algorithm();
let digest_size = algorithm.output_len().next_power_of_two();
let multiplier = 1024u64;
level_data
@@ -222,7 +224,8 @@ impl<'a> HashTree<'a> {
level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
let digest_size = self.algorithm.output_len().next_power_of_two();
let algorithm = self.salted_context.algorithm();
let digest_size = algorithm.output_len().next_power_of_two();
level_data
.par_chunks_exact_mut(digest_size)
@@ -263,8 +266,7 @@ impl<'a> HashTree<'a> {
let mut buf = vec![0u8; image_size as usize];
reader.read_exact(&mut buf)?;
let mut context = Context::new(self.algorithm);
context.update(self.salt);
let mut context = self.salted_context.clone();
context.update(&buf);
let digest = context.finish();
@@ -309,8 +311,7 @@ impl<'a> HashTree<'a> {
}
// Calculate the root hash.
let mut context = Context::new(self.algorithm);
context.update(self.salt);
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();
@@ -326,7 +327,7 @@ impl<'a> HashTree<'a> {
cancel_signal: &AtomicBool,
) -> Result<(Vec<u8>, Vec<u8>)> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.get(0).map(|r| r.end).unwrap_or(0);
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(
@@ -352,7 +353,7 @@ impl<'a> HashTree<'a> {
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.get(0).map(|r| r.end).unwrap_or(0);
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,
@@ -381,7 +382,7 @@ impl<'a> HashTree<'a> {
cancel_signal: &AtomicBool,
) -> Result<()> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.get(0).map(|r| r.end).unwrap_or(0);
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,
@@ -402,8 +403,9 @@ impl<'a> HashTree<'a> {
if hash_tree_data != actual_hash_tree_data {
// These are multiple megabytes, so only report the hashes.
let expected = ring::digest::digest(self.algorithm, hash_tree_data);
let actual = ring::digest::digest(self.algorithm, &actual_hash_tree_data);
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),
@@ -637,7 +639,7 @@ mod tests {
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, &[0..0]).unwrap(), &[]);
assert_eq!(
hash_tree
.blocks_for_ranges(16384, &[12287..12289, 0..1, 5000..5001])
+5 -5
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -152,15 +152,15 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
}
"post-build-incremental" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
value.clone_into(&mut p.build_incremental);
}
"post-sdk-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.sdk_level = value.to_owned();
value.clone_into(&mut p.sdk_level);
}
"post-security-patch-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.security_patch_level = value.to_owned();
value.clone_into(&mut p.security_patch_level);
}
"post-timestamp" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
@@ -176,7 +176,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
}
"pre-build-incremental" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
value.clone_into(&mut p.build_incremental);
}
"spl-downgrade" => metadata.spl_downgrade = parse_yes()?,
k if k.ends_with("-property-files") => {
+8 -8
View File
@@ -14,6 +14,11 @@ 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::{
@@ -25,11 +30,6 @@ 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,
@@ -99,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")]
@@ -918,7 +918,7 @@ pub fn compress_image(
});
}
let chunks_total = util::div_ceil(file_size, CHUNK_SIZE);
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
let mut bytes_compressed = 0;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut operations = vec![];
@@ -1059,7 +1059,7 @@ pub fn compress_modified_image(
return Err(Error::ExtentsNotInOrder);
}
let groups_total = util::div_ceil(operations.len(), OPERATION_GROUP);
let groups_total = operations.len().div_ceil(OPERATION_GROUP);
let mut bytes_compressed = 0;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut modified_operations = vec![];
+15 -3
View File
@@ -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);
}
}
}
+316 -64
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -16,16 +16,17 @@ use std::{
};
use bstr::ByteSlice;
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
use regex::bytes::Regex;
use ring::digest::Context;
use rsa::RsaPrivateKey;
use thiserror::Error;
use x509_cert::Certificate;
use xz2::{
use liblzma::{
stream::{Check, Stream},
write::XzEncoder,
};
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
use regex::bytes::Regex;
use ring::digest::Context;
use rsa::{RsaPrivateKey, RsaPublicKey};
use thiserror::Error;
use tracing::{debug, debug_span, trace, warn, Span};
use x509_cert::Certificate;
use zip::{result::ZipError, ZipArchive};
use crate::{
@@ -67,9 +68,13 @@ pub enum 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),
Zip(#[source] ZipError),
#[error("Zip error for entry name: {0:?}")]
ZipEntryName(String, #[source] ZipError),
#[error("Zip error for entry index #{0}")]
ZipEntryIndex(usize, #[source] ZipError),
#[error("I/O error")]
Io(#[from] io::Error),
#[error("File I/O error")]
@@ -86,6 +91,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()))
}
@@ -98,6 +109,8 @@ 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())
}
@@ -140,20 +153,31 @@ 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: &'static [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;
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
const ZIP_LIBMAGISK32: &'static str = "lib/armeabi-v7a/libmagisk32.so";
const ZIP_LIBMAGISK64: &'static str = "lib/arm64-v8a/libmagisk64.so";
const ZIP_MAGISKINIT: &'static str = "lib/arm64-v8a/libmagiskinit.so";
const ZIP_STUB: &'static str = "assets/stub.apk";
const ZIP_UTIL_FUNCTIONS: &'static str = "assets/util_functions.sh";
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: {:?})",
@@ -162,7 +186,7 @@ impl MagiskRootPatcher {
);
if ignore_compatibility {
warning_fn(&msg);
warn!("{msg}");
} else {
return Err(Error::Validation(msg));
}
@@ -176,7 +200,7 @@ impl MagiskRootPatcher {
);
if ignore_compatibility {
warning_fn(&msg);
warn!("{msg}");
} else {
return Err(Error::Validation(msg));
}
@@ -195,8 +219,10 @@ impl MagiskRootPatcher {
fn get_version(path: &Path) -> Result<u32> {
let reader = File::open(path).map_err(|e| Error::File(path.to_owned(), e))?;
let reader = BufReader::new(reader);
let mut zip = ZipArchive::new(reader)?;
let entry = zip.by_name("assets/util_functions.sh")?;
let mut zip = ZipArchive::new(reader).map_err(Error::Zip)?;
let entry = zip
.by_name(Self::ZIP_UTIL_FUNCTIONS)
.map_err(|e| Error::ZipEntryName(Self::ZIP_UTIL_FUNCTIONS.to_owned(), e))?;
let mut entry = BufReader::new(entry);
let mut line = String::new();
@@ -208,6 +234,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))?;
@@ -216,6 +244,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
@@ -223,7 +263,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);
@@ -268,11 +313,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(
@@ -280,6 +368,8 @@ impl MagiskRootPatcher {
0,
CpioEntryData::Data(rm_list),
));
Ok(())
}
}
@@ -307,7 +397,7 @@ impl BootImagePatch for MagiskRootPatcher {
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))?;
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
let mut zip = ZipArchive::new(BufReader::new(zip_reader)).map_err(Error::Zip)?;
// Load the first ramdisk. If it doesn't exist, we have to generate one
// from scratch.
@@ -336,7 +426,9 @@ impl BootImagePatch for MagiskRootPatcher {
// Add magiskinit.
{
let mut zip_entry = zip.by_name("lib/arm64-v8a/libmagiskinit.so")?;
let mut zip_entry = zip
.by_name(Self::ZIP_MAGISKINIT)
.map_err(|e| Error::ZipEntryName(Self::ZIP_MAGISKINIT.to_owned(), e))?;
let mut data = vec![];
zip_entry.read_to_end(&mut data)?;
@@ -347,48 +439,55 @@ impl BootImagePatch for MagiskRootPatcher {
));
}
// Add xz-compressed magisk32 and magisk64.
let mut xz_files = HashMap::<&str, &[u8]>::new();
xz_files.insert(
"lib/armeabi-v7a/libmagisk32.so",
b"overlay.d/sbin/magisk32.xz",
);
xz_files.insert(
"lib/arm64-v8a/libmagisk64.so",
b"overlay.d/sbin/magisk64.xz",
);
if zip.file_names().any(|n| n == Self::ZIP_LIBMAGISK) {
// Newer Magisk versions only include a single binary for the target
// ABI in the ramdisk. fb5ee86615ed3df830e8538f8b39b1b133caea34.
debug!("Single libmagisk");
xz_files.insert(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz");
} else {
// Older Magisk versions include the 64-bit binary and, optionally,
// the 32-bit binary if the device supports it. We unconditionally
// include the magisk32 because the boot image itself doesn't have
// sufficient information to determine if a device is 64-bit only.
debug!("Split libmagisk32/libmagisk64");
xz_files.insert(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz");
xz_files.insert(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz");
}
// Add stub apk, which only exists after Magisk commit
// ad0e6511e11ebec65aa9b5b916e1397342850319.
if zip.file_names().any(|n| n == "assets/stub.apk") {
xz_files.insert("assets/stub.apk", b"overlay.d/sbin/stub.xz");
if zip.file_names().any(|n| n == Self::ZIP_STUB) {
debug!("Magisk stub found");
xz_files.insert(Self::ZIP_STUB, 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 reader = zip
.by_name(source)
.map_err(|e| Error::ZipEntryName(source.to_owned(), e))?;
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) {
@@ -408,6 +507,8 @@ impl BootImagePatch 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,
@@ -483,11 +584,14 @@ impl OtaCertPatcher {
continue;
};
let mut zip = ZipArchive::new(Cursor::new(&data))?;
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(Error::Zip)?;
for index in 0..zip.len() {
let zip_entry = zip.by_index(index)?;
let zip_entry = zip
.by_index(index)
.map_err(|e| Error::ZipEntryIndex(index, e))?;
if !zip_entry.name().ends_with(".x509.pem") {
debug!("Skipping invalid entry path: {}", zip_entry.name());
continue;
}
@@ -564,6 +668,7 @@ impl BootImagePatch for OtaCertPatcher {
};
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() {
@@ -584,6 +689,137 @@ impl BootImagePatch for OtaCertPatcher {
}
}
/// Add the AVB public key to DSU's list of trusted keys for verifying GSIs.
pub struct DsuPubKeyPatcher {
key: RsaPublicKey,
}
impl DsuPubKeyPatcher {
const FIRST_STAGE_PATH: &'static [u8] = b"first_stage_ramdisk";
const DSU_KEYS_PATH: &'static [u8] = b"first_stage_ramdisk/avb";
const AVBROOT_KEY_PATH: &'static [u8] = b"first_stage_ramdisk/avb/avbroot.avbpubkey";
pub fn new(key: RsaPublicKey) -> Self {
Self { key }
}
fn patch_ramdisk(&self, ramdisk: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
if !entries.iter_mut().any(|e| e.path == Self::FIRST_STAGE_PATH) {
return Ok(false);
}
if !entries.iter().any(|e| e.path == Self::DSU_KEYS_PATH) {
entries.push(CpioEntry::new_directory(Self::DSU_KEYS_PATH, 0o755));
}
let data = CpioEntryData::Data(avb::encode_public_key(&self.key)?);
if let Some(e) = entries
.iter_mut()
.find(|e| e.path == Self::AVBROOT_KEY_PATH)
{
e.data = data;
} else {
entries.push(CpioEntry::new_file(Self::AVBROOT_KEY_PATH, 0o644, data));
};
*ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
Ok(true)
}
}
impl BootImagePatch for DsuPubKeyPatcher {
fn patcher_name(&self) -> &'static str {
"DsuPubKeyPatcher"
}
fn find_targets<'a>(
&self,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> Result<Vec<&'a str>> {
let mut dsu_keys_targets = vec![];
let mut first_stage_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;
}
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
let mut found = false;
for entry in entries {
if entry.path == Self::DSU_KEYS_PATH {
dsu_keys_targets.push(*name);
found = true;
} else if entry.path == Self::FIRST_STAGE_PATH {
first_stage_targets.push(*name);
found = true;
}
}
if found {
continue 'outer;
}
}
}
if !dsu_keys_targets.is_empty() {
// Most builds trust as least one DSU key. For these builds, add the
// user's key to the same directory.
if dsu_keys_targets.len() > 1 {
return Err(Error::Validation(format!(
"DSU keys found in more than one boot image: {dsu_keys_targets:?}",
)));
}
Ok(dsu_keys_targets)
} else {
// For builds that don't trust any DSU keys, pick the first boot
// image that contains a first stage ramdisk directory.
if !first_stage_targets.is_empty() {
first_stage_targets.sort();
first_stage_targets.resize(1, "");
}
Ok(first_stage_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,
};
for ramdisk in ramdisks {
if ramdisk.is_empty() {
continue;
}
if self.patch_ramdisk(ramdisk, cancel_signal)? {
return Ok(());
}
}
Err(Error::Validation(format!(
"No ramdisk contains {:?}",
Self::FIRST_STAGE_PATH.as_bstr(),
)))
}
}
/// Replace the boot image with a prepatched boot image if it is compatible.
///
/// An image is compatible if all the non-size-related header fields are
@@ -593,7 +829,6 @@ impl BootImagePatch for OtaCertPatcher {
pub struct PrepatchedImagePatcher {
prepatched: PathBuf,
fatal_level: u8,
warning_fn: Box<dyn Fn(&str) + Send + Sync>,
}
impl PrepatchedImagePatcher {
@@ -604,15 +839,10 @@ impl PrepatchedImagePatcher {
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 + Sync + 'static,
) -> Self {
pub fn new(prepatched: &Path, fatal_level: u8) -> Self {
Self {
prepatched: prepatched.to_owned(),
fatal_level,
warning_fn: Box::new(warning_fn),
}
}
@@ -829,7 +1059,7 @@ impl BootImagePatch for PrepatchedImagePatcher {
msg.push_str(warning);
}
(self.warning_fn)(&msg);
warn!("{msg}");
}
if !errors.is_empty() {
@@ -853,9 +1083,12 @@ 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();
names
.par_iter()
.map(|name| {
let _span = debug_span!(parent: &parent_span, "image", name).entered();
let mut reader = open_input(name)?;
let (header, footer, image_size) = avb::load_image(&mut reader)?;
@@ -873,6 +1106,8 @@ pub fn load_boot_images<'a>(
boot_image,
};
trace!("Loaded {image_size} byte boot image: {name}");
Ok((*name, info))
})
.collect()
@@ -891,6 +1126,8 @@ pub fn patch_boot_images<'a>(
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)?;
@@ -898,16 +1135,21 @@ pub fn patch_boot_images<'a>(
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()) {
@@ -927,15 +1169,19 @@ pub fn patch_boot_images<'a>(
groups
.par_iter_mut()
.try_for_each(|(_, (info, patchers))| -> Result<()> {
patchers
.iter()
.try_for_each(|p| p.patch(&mut info.boot_image, cancel_signal))
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);
@@ -951,15 +1197,21 @@ pub fn patch_boot_images<'a>(
let (mut writer, context) = hashing_writer.finish();
descriptor.image_size = writer.stream_position()?;
descriptor.hash_algorithm = "sha256".to_owned();
"sha256".clone_into(&mut descriptor.hash_algorithm);
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)?;
avb::write_appended_image(
writer,
&info.header,
&mut info.footer,
Some(info.image_size),
)?;
Ok(())
})
+5
View File
@@ -7,6 +7,7 @@ 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};
@@ -134,8 +135,12 @@ pub fn create_zip_with_size(cert: &Certificate, size: usize) -> Result<Vec<u8>>
] {
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);
}
+27 -4
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -13,6 +13,7 @@ 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;
@@ -50,6 +51,7 @@ type Result<T> = std::result::Result<T, Error>;
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;
}
@@ -60,14 +62,18 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
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;
}
@@ -76,10 +82,13 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
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)
}
@@ -110,6 +119,8 @@ pub fn patch_system_image(
// 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);
@@ -118,7 +129,8 @@ pub fn patch_system_image(
return Err(Error::NoHashTreeDescriptor);
};
let num_chunks = util::div_ceil(footer.original_image_size, CHUNK_SIZE);
let num_chunks = footer.original_image_size.div_ceil(CHUNK_SIZE);
trace!("Parallel heuristics search for otacerts.zip with {num_chunks} chunks");
let modified_ranges = (0..num_chunks)
.into_par_iter()
@@ -137,6 +149,9 @@ pub fn patch_system_image(
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;
};
@@ -168,7 +183,14 @@ pub fn patch_system_image(
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.
descriptor.hash_algorithm = "sha256".to_owned();
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
@@ -179,12 +201,13 @@ pub fn patch_system_image(
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)?;
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
return Err(Error::NoHashTreeDescriptor);
-10
View File
@@ -49,16 +49,6 @@ 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
+393 -108
View File
@@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
io::{Cursor, Read, Seek, SeekFrom, Write},
io::{Cursor, Read, Seek, Write},
sync::atomic::AtomicBool,
};
@@ -14,8 +14,12 @@ use rsa::RsaPrivateKey;
use avbroot::{
self,
format::avb::{self, AppendedDescriptorMut, AppendedDescriptorRef},
stream::{self, SharedCursor},
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
KernelCmdlineDescriptor, PropertyDescriptor,
},
stream::SharedCursor,
};
fn get_test_key() -> RsaPrivateKey {
@@ -31,154 +35,435 @@ fn get_test_key() -> RsaPrivateKey {
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
}
fn repeat_str(s: &str, max_len: usize) -> String {
assert!(!s.is_empty());
let mut result = s.repeat(max_len / s.len());
result.push_str(&s[..max_len % s.len()]);
result
}
fn repeat_array<const N: usize>(data: &[u8]) -> [u8; N] {
assert!(!data.is_empty());
let mut result = [0u8; N];
for i in 0..N / data.len() {
result[i * data.len()..][..data.len()].copy_from_slice(data);
}
let remain = N % data.len();
result[N - remain..].copy_from_slice(&data[..remain]);
result
}
#[test]
fn round_trip_root_image() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vbmeta_root.img",
));
let reader = Cursor::new(data);
let (mut header, footer, _) = avb::load_image(reader).unwrap();
assert_matches!(footer, None);
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::HashTree(HashTreeDescriptor {
dm_verity_version: 1,
image_size: 4096,
tree_offset: 0,
tree_size: 2048,
data_block_size: 4096,
hash_block_size: 4096,
fec_num_roots: 1,
fec_offset: 2048,
fec_size: 2048,
hash_algorithm: "sha512".to_owned(),
partition_name: "hashtreed_partition".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(8),
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(8),
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
Descriptor::Hash(HashDescriptor {
image_size: 6,
hash_algorithm: "sha256".to_owned(),
partition_name: "hashed_partition".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(4),
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
Descriptor::KernelCmdline(KernelCmdlineDescriptor {
flags: 1,
cmdline: "foobar".to_owned(),
}),
Descriptor::ChainPartition(ChainPartitionDescriptor {
rollback_index_location: 1,
partition_name: "chained_partition".to_owned(),
public_key: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(129),
flags: 0xfedcba98,
reserved: repeat_array(&[0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Clear out the signature-related fields and re-sign.
header.hash.clear();
header.signature.clear();
header.public_key.clear();
header.sign(&key).unwrap();
// Write vbmeta structures.
let mut writer = Cursor::new(Vec::new());
avb::write_root_image(&mut writer, &header, 64).unwrap();
let new_data = writer.into_inner();
let data = writer.into_inner();
assert_eq!(data, new_data.as_slice());
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
assert_matches!(new_footer, None);
assert_eq!(new_header, header);
assert_eq!(new_image_size, data.len() as u64);
}
#[test]
fn round_trip_appended_hash_image() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vbmeta_appended_hash.img",
));
let mut reader = Cursor::new(data);
let cancel_signal = AtomicBool::new(false);
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
let mut footer = footer.unwrap();
let key = get_test_key();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Verify the digest.
match header.appended_descriptor().unwrap() {
AppendedDescriptorRef::HashTree(_) => panic!("Expected hash descriptor"),
AppendedDescriptorRef::Hash(d) => {
reader.rewind().unwrap();
d.verify(&mut reader, &cancel_signal).unwrap();
}
}
let image_size = 12288;
let raw_data = b"foobar";
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::Hash(HashDescriptor {
image_size: raw_data.len() as u64,
hash_algorithm: "sha256".to_owned(),
partition_name: "vbmeta_appended_hash".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: vec![], // autogenerated
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
version_major: 1,
version_minor: 0,
original_image_size: 0, // autogenerated
vbmeta_offset: 0, // autogenerated
vbmeta_size: 0, // autogenerated
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = Cursor::new(Vec::new());
let cancel_signal = AtomicBool::new(false);
// Copy the partition data.
reader.seek(SeekFrom::Start(0)).unwrap();
stream::copy_n(
&mut reader,
&mut writer,
footer.original_image_size,
&cancel_signal,
)
.unwrap();
// Write the raw partition data.
writer.write_all(raw_data).unwrap();
// Regenerate the digest.
// Regenerate the raw image digest.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(_) => panic!("Expected hash descriptor"),
AppendedDescriptorMut::Hash(d) => {
d.root_digest.clear();
writer.rewind().unwrap();
d.update(&mut writer, &cancel_signal).unwrap();
}
}
// Clear out the signature-related fields and re-sign.
header.hash.clear();
header.signature.clear();
header.public_key.clear();
// Verify the raw image digest.
match header.appended_descriptor().unwrap() {
AppendedDescriptorRef::HashTree(_) => panic!("Expected hash descriptor"),
AppendedDescriptorRef::Hash(d) => {
writer.rewind().unwrap();
d.verify(&mut writer, &cancel_signal).unwrap();
}
}
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write new vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
let new_data = writer.into_inner();
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
let data = writer.into_inner();
assert_eq!(data, new_data.as_slice());
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
let new_footer = new_footer.unwrap();
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, image_size);
}
#[test]
fn round_trip_appended_hash_tree_image() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vbmeta_appended_hash_tree.img",
));
let mut reader = SharedCursor::default();
reader.write_all(data).unwrap();
let cancel_signal = AtomicBool::new(false);
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
let mut footer = footer.unwrap();
let key = get_test_key();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Verify the hash tree and FEC data.
match header.appended_descriptor().unwrap() {
AppendedDescriptorRef::HashTree(d) => d.verify(&reader, &cancel_signal).unwrap(),
AppendedDescriptorRef::Hash(_) => panic!("Expected hash tree descriptor"),
}
fn round_trip_appended_hash_tree_image_fixed_size() {
let image_size = 32768;
let raw_data: [u8; 8192] = repeat_array(b"foobar");
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::HashTree(HashTreeDescriptor {
dm_verity_version: 1,
image_size: raw_data.len() as u64,
tree_offset: 0, // autogenerated
tree_size: 0, // autogenerated
data_block_size: 4096,
hash_block_size: 4096,
fec_num_roots: 2,
fec_offset: 0, // autogenerated
fec_size: 0, // autogenerated
hash_algorithm: "sha256".to_owned(),
partition_name: "vbmeta_appended_hash_tree".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: vec![], // autogenerated
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
version_major: 1,
version_minor: 0,
original_image_size: 0, // autogenerated
vbmeta_offset: 0, // autogenerated
vbmeta_size: 0, // autogenerated
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = SharedCursor::default();
let cancel_signal = AtomicBool::new(false);
// Copy the partition data, excluding the hash tree and FEC data.
reader.seek(SeekFrom::Start(0)).unwrap();
stream::copy_n(
&mut reader,
&mut writer,
footer.original_image_size,
&cancel_signal,
)
.unwrap();
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
// Regenerate the hash tree and FEC data.
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.root_digest.clear();
d.tree_offset = 0;
d.tree_size = 0;
d.fec_offset = 0;
d.fec_size = 0;
d.update(&writer, &writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Clear out the signature-related fields and re-sign.
header.hash.clear();
header.signature.clear();
header.public_key.clear();
// Verify the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.verify(&writer, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write new vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
let mut new_data = Vec::new();
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut new_data).unwrap();
writer.read_to_end(&mut data).unwrap();
assert_eq!(data, new_data.as_slice());
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
let new_footer = new_footer.unwrap();
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, image_size);
}
#[test]
fn round_trip_appended_hash_tree_image_minimum_size() {
let raw_data: [u8; 8192] = repeat_array(b"foobar");
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::HashTree(HashTreeDescriptor {
dm_verity_version: 1,
image_size: raw_data.len() as u64,
tree_offset: 0, // autogenerated
tree_size: 0, // autogenerated
data_block_size: 4096,
hash_block_size: 4096,
fec_num_roots: 2,
fec_offset: 0, // autogenerated
fec_size: 0, // autogenerated
hash_algorithm: "sha256".to_owned(),
partition_name: "vbmeta_appended_hash_tree".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: vec![], // autogenerated
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
version_major: 1,
version_minor: 0,
original_image_size: 0, // autogenerated
vbmeta_offset: 0, // autogenerated
vbmeta_size: 0, // autogenerated
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = SharedCursor::default();
let cancel_signal = AtomicBool::new(false);
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.update(&writer, &writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Verify the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.verify(&writer, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut data).unwrap();
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
let new_footer = new_footer.unwrap();
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, 28672);
}
+284 -60
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -7,7 +7,13 @@ use std::io::Cursor;
use avbroot::{
self,
format::bootimage::{BootImage, BootImageExt},
format::{
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
bootimage::{
self, BootImage, BootImageExt, BootImageV0Through2, BootImageV3Through4, RamdiskMeta,
V1Extra, V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
},
},
stream::{FromReader, ToWriter},
};
use pkcs8::DecodePrivateKey;
@@ -26,100 +32,318 @@ fn get_test_key() -> RsaPrivateKey {
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
}
fn round_trip(data: &[u8], expected_version: u32) {
let reader = Cursor::new(data);
let mut image = BootImage::from_reader(reader).unwrap();
fn repeat(s: &str, max_len: usize) -> String {
assert!(!s.is_empty());
let mut result = s.repeat(max_len / s.len());
result.push_str(&s[..max_len % s.len()]);
result
}
fn round_trip(image: &BootImage, sha512: &[u8; 64], expected_version: u32) {
assert_eq!(image.header_version(), expected_version);
match &mut image {
BootImage::V3Through4(b) => {
let should_sign = b
.v4_extra
.as_ref()
.map_or(false, |v4| v4.signature.is_some());
let key = get_test_key();
let signed = b.sign(&key).unwrap();
assert_eq!(signed, should_sign);
}
_ => {}
}
let mut writer = Cursor::new(Vec::new());
image.to_writer(&mut writer).unwrap();
let new_data = writer.into_inner();
let data = writer.into_inner();
assert_eq!(data, new_data);
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
sha512,
);
let reader = Cursor::new(data);
let new_image = BootImage::from_reader(reader).unwrap();
assert_eq!(&new_image, image);
}
#[test]
fn round_trip_v0() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boot_v0.img",
));
round_trip(data, 0);
let image = BootImage::V0Through2(BootImageV0Through2 {
kernel_addr: 0x01234567,
ramdisk_addr: 0x89abcdef,
second_addr: 0x02468ace,
tags_addr: 0x13579bdf,
page_size: 4096,
os_version: 0x76543210,
name: repeat("Name", 16),
cmdline: repeat("Cmdline", 512),
id: [
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
0x33221100,
],
extra_cmdline: repeat("ExtraCmdline", 1024),
kernel: b"kernel data".to_vec(),
ramdisk: b"ramdisk data".to_vec(),
second: b"second data".to_vec(),
v1_extra: None,
v2_extra: None,
});
let sha512 = [
0x23, 0x65, 0x0b, 0xfa, 0x7a, 0x09, 0x0a, 0xdf, 0xdd, 0x9a, 0x6c, 0x03, 0xfa, 0xc5, 0xe1,
0xfa, 0x27, 0x65, 0xa0, 0x94, 0xef, 0xa2, 0x0c, 0xc5, 0x3e, 0xd9, 0x67, 0x7d, 0x88, 0x7b,
0xb3, 0x48, 0x39, 0xab, 0x28, 0x77, 0x7b, 0x18, 0xec, 0x60, 0xe0, 0xb7, 0x0d, 0x15, 0x26,
0xb2, 0xd4, 0x27, 0x25, 0x92, 0x5c, 0x7b, 0x0b, 0x5c, 0xf7, 0xed, 0x27, 0x6c, 0x39, 0xb5,
0xb7, 0x44, 0xbb, 0xec,
];
round_trip(&image, &sha512, 0);
}
#[test]
fn round_trip_v1() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boot_v1.img",
));
round_trip(data, 1);
let image = BootImage::V0Through2(BootImageV0Through2 {
kernel_addr: 0x01234567,
ramdisk_addr: 0x89abcdef,
second_addr: 0x02468ace,
tags_addr: 0x13579bdf,
page_size: 4096,
os_version: 0x76543210,
name: repeat("Name", 16),
cmdline: repeat("Cmdline", 512),
id: [
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
0x33221100,
],
extra_cmdline: repeat("ExtraCmdline", 1024),
kernel: b"kernel data".to_vec(),
ramdisk: b"ramdisk data".to_vec(),
second: b"second data".to_vec(),
v1_extra: Some(V1Extra {
recovery_dtbo_offset: 0x0123456789abcdef,
recovery_dtbo: b"recovery_dtbo data".to_vec(),
}),
v2_extra: None,
});
let sha512 = [
0x37, 0x8e, 0xf1, 0xf0, 0xb8, 0x44, 0x0f, 0x9e, 0x16, 0xc0, 0x15, 0x98, 0xa2, 0xb5, 0x06,
0x63, 0x59, 0xf4, 0x91, 0xb6, 0x28, 0x03, 0xe6, 0xdc, 0xd2, 0x0d, 0xd7, 0x49, 0x33, 0x63,
0x91, 0xd4, 0xa8, 0x24, 0xff, 0xb0, 0x5f, 0x99, 0x2a, 0x9a, 0xb3, 0x66, 0x81, 0x41, 0x69,
0xb0, 0xbc, 0xe2, 0x5b, 0x33, 0x3f, 0x39, 0x6a, 0xa8, 0xbd, 0xe1, 0x15, 0x3e, 0x51, 0x5a,
0x2a, 0x9d, 0x23, 0x90,
];
round_trip(&image, &sha512, 1);
}
#[test]
fn round_trip_v2() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boot_v2.img",
));
round_trip(data, 2);
let image = BootImage::V0Through2(BootImageV0Through2 {
kernel_addr: 0x01234567,
ramdisk_addr: 0x89abcdef,
second_addr: 0x02468ace,
tags_addr: 0x13579bdf,
page_size: 4096,
os_version: 0x76543210,
name: repeat("Name", 16),
cmdline: repeat("Cmdline", 512),
id: [
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
0x33221100,
],
extra_cmdline: repeat("ExtraCmdline", 1024),
kernel: b"kernel data".to_vec(),
ramdisk: b"ramdisk data".to_vec(),
second: b"second data".to_vec(),
v1_extra: Some(V1Extra {
recovery_dtbo_offset: 0x0123456789abcdef,
recovery_dtbo: b"recovery_dtbo data".to_vec(),
}),
v2_extra: Some(V2Extra {
dtb_addr: 0xfedcba9876543210,
dtb: b"dtb data".to_vec(),
}),
});
let sha512 = [
0x04, 0x24, 0x5b, 0xb7, 0x07, 0x82, 0xa9, 0x08, 0x68, 0xb9, 0xc9, 0x65, 0x1f, 0x53, 0xd7,
0x6c, 0xcf, 0xf3, 0x48, 0x58, 0x9a, 0xd4, 0xb1, 0xf3, 0xd8, 0x6f, 0x95, 0x10, 0x70, 0x2f,
0x53, 0x30, 0x60, 0x60, 0xe4, 0x68, 0xd9, 0x84, 0xe8, 0x0a, 0xf3, 0x12, 0xb3, 0xa3, 0x1b,
0x06, 0x88, 0x2f, 0x5d, 0x34, 0x5a, 0xea, 0x8f, 0xbb, 0x54, 0x49, 0x3c, 0xc1, 0x9c, 0xc6,
0x24, 0x80, 0x03, 0xde,
];
round_trip(&image, &sha512, 2);
}
#[test]
fn round_trip_v3() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boot_v3.img",
));
round_trip(data, 3);
let image = BootImage::V3Through4(BootImageV3Through4 {
os_version: 0x01234567,
reserved: [0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff],
cmdline: repeat("Cmdline", 1536),
v4_extra: None,
kernel: b"kernel data".to_vec(),
ramdisk: b"ramdisk data".to_vec(),
});
let sha512 = [
0x30, 0xea, 0x77, 0x0a, 0xd3, 0x24, 0x6a, 0x3f, 0xf8, 0xdf, 0xe6, 0xd9, 0x5a, 0xa1, 0xd3,
0xa4, 0x3b, 0x8a, 0x13, 0x39, 0x5e, 0x58, 0x24, 0x3e, 0x71, 0x31, 0x78, 0xa1, 0x2c, 0xad,
0x1d, 0xca, 0x24, 0x12, 0xf5, 0xfb, 0x2c, 0x48, 0xa5, 0x3d, 0xc0, 0x38, 0x55, 0xb6, 0xfd,
0xd3, 0x30, 0xe0, 0x69, 0x11, 0x28, 0xd7, 0x29, 0xda, 0x2e, 0x5a, 0x49, 0x5c, 0x39, 0x1d,
0xb9, 0xdb, 0x53, 0xe1,
];
round_trip(&image, &sha512, 3);
}
#[test]
fn round_trip_v4() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boot_v4.img",
));
round_trip(data, 4);
let image = BootImage::V3Through4(BootImageV3Through4 {
os_version: 0x01234567,
reserved: [0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff],
cmdline: repeat("Cmdline", 1536),
v4_extra: Some(V4Extra { signature: None }),
kernel: b"kernel data".to_vec(),
ramdisk: b"ramdisk data".to_vec(),
});
let sha512 = [
0xa8, 0x1d, 0x2b, 0x78, 0x22, 0x45, 0x0b, 0xe7, 0xc2, 0x3a, 0xd8, 0xda, 0x95, 0x49, 0x77,
0x18, 0xd0, 0x7b, 0x9b, 0x7f, 0xc7, 0xf6, 0x48, 0xb4, 0x2d, 0x85, 0x6d, 0xe3, 0x5a, 0xa3,
0x24, 0xb6, 0x94, 0x56, 0xb9, 0x07, 0x84, 0xdb, 0x50, 0x01, 0xca, 0x6c, 0x86, 0x26, 0x32,
0x79, 0x0c, 0xc5, 0x70, 0xcf, 0xcc, 0x7f, 0xc3, 0x5b, 0x96, 0x56, 0x23, 0x5c, 0xd0, 0x50,
0xb0, 0x98, 0xdb, 0x4a,
];
round_trip(&image, &sha512, 4);
}
#[test]
fn round_trip_v4_vts() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boot_v4_vts.img",
));
round_trip(data, 4);
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![],
descriptors: vec![Descriptor::Hash(HashDescriptor {
image_size: 12288,
hash_algorithm: "sha256".to_owned(),
partition_name: "boot".to_owned(),
salt: vec![0x64, 0x30, 0x30, 0x64, 0x66, 0x30, 0x30, 0x64],
root_digest: vec![
0xab, 0xd5, 0x48, 0x3e, 0x11, 0xe7, 0x94, 0x0c, 0xb9, 0xbf, 0x38, 0x75, 0x87, 0xa4,
0xa1, 0x65, 0x99, 0x81, 0xa1, 0xb8, 0x39, 0x62, 0xb7, 0xc1, 0xfa, 0xf1, 0xb0, 0xcd,
0x63, 0x07, 0xd4, 0x49,
],
flags: 0,
reserved: [0; 60],
})],
rollback_index: 0,
flags: 0,
rollback_index_location: 0,
release_string: "avbtool 1.2.0".to_owned(),
reserved: [0; 80],
};
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
let image = BootImage::V3Through4(BootImageV3Through4 {
os_version: 0x01234567,
reserved: [0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff],
cmdline: repeat("Cmdline", 1536),
v4_extra: Some(V4Extra {
signature: Some(header),
}),
kernel: b"kernel data".to_vec(),
ramdisk: b"ramdisk data".to_vec(),
});
let sha512 = [
0x19, 0x47, 0x15, 0x3c, 0x1f, 0x62, 0x84, 0xee, 0xbc, 0x16, 0x9e, 0x5a, 0xf2, 0x45, 0x2a,
0xf7, 0x40, 0xc2, 0x18, 0x7f, 0x23, 0xb2, 0xa4, 0x20, 0x10, 0xdf, 0xb1, 0x5c, 0xf2, 0x7f,
0x6f, 0x79, 0x22, 0x1d, 0x29, 0x27, 0x78, 0xea, 0xb3, 0x9e, 0x1f, 0xfe, 0xeb, 0xc8, 0x9f,
0xe6, 0xef, 0xce, 0xa2, 0x28, 0x0b, 0x05, 0x1d, 0x52, 0xff, 0xab, 0xd4, 0x6f, 0x87, 0x11,
0xd9, 0xb6, 0x5f, 0x2e,
];
round_trip(&image, &sha512, 4);
}
#[test]
fn round_trip_vendor_v3() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vendor_v3.img",
));
round_trip(data, 3);
let image = BootImage::VendorV3Through4(VendorBootImageV3Through4 {
page_size: 4096,
kernel_addr: 0x01234567,
ramdisk_addr: 0x89abcdef,
cmdline: repeat("Cmdline", 2048),
tags_addr: 0xfedcba98,
name: repeat("Name", 16),
dtb: b"dtb data".to_vec(),
dtb_addr: 0x76543210,
ramdisks: vec![b"ramdisk data".to_vec()],
v4_extra: None,
});
let sha512 = [
0x17, 0x18, 0xb9, 0x67, 0x4c, 0x82, 0x71, 0x98, 0x6a, 0x8a, 0xb8, 0x85, 0x3c, 0x77, 0x9e,
0x27, 0xeb, 0xce, 0x2a, 0x23, 0x04, 0x63, 0x7c, 0x94, 0xd4, 0xad, 0x1f, 0x3c, 0xee, 0x7e,
0x41, 0x8b, 0xa8, 0xd9, 0x35, 0xec, 0xf2, 0xc1, 0x52, 0x3a, 0xd9, 0x5b, 0xbe, 0x63, 0xe8,
0x00, 0xd2, 0x23, 0x4e, 0x37, 0x76, 0x31, 0x5a, 0xfc, 0x63, 0x43, 0x32, 0x34, 0x30, 0xf6,
0x3e, 0x2e, 0x3e, 0x66,
];
round_trip(&image, &sha512, 3);
}
#[test]
fn round_trip_vendor_v4() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vendor_v4.img",
));
round_trip(data, 4);
let board_id = [
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
0x33221100, 0x004488cc, 0x115599dd, 0x2266aaee, 0x3377bbff, 0xffbb7733, 0xeeaa6622,
0xdd995511, 0xcc884400,
];
let image = BootImage::VendorV3Through4(VendorBootImageV3Through4 {
page_size: 2048,
kernel_addr: 0x01234567,
ramdisk_addr: 0x89abcdef,
cmdline: repeat("Cmdline", 2048),
tags_addr: 0xfedcba98,
name: repeat("Name", 16),
dtb: b"dtb data".to_vec(),
dtb_addr: 0x76543210,
ramdisks: vec![
b"ramdisk 0 data".to_vec(),
b"ramdisk 1 data".to_vec(),
b"ramdisk 2 data".to_vec(),
b"ramdisk 3 data".to_vec(),
],
v4_extra: Some(VendorV4Extra {
ramdisk_metas: vec![
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_NONE,
ramdisk_name: repeat("None", 32),
board_id,
},
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_PLATFORM,
ramdisk_name: repeat("Platform", 32),
board_id,
},
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_RECOVERY,
ramdisk_name: repeat("Recovery", 32),
board_id,
},
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
ramdisk_name: repeat("Dlkm", 32),
board_id,
},
],
bootconfig: "bootconfig data".to_owned(),
}),
});
let sha512 = [
0x0e, 0x3f, 0x86, 0x9e, 0xad, 0x98, 0xbb, 0x53, 0xc7, 0xc4, 0x3f, 0xb8, 0xc6, 0x06, 0xdc,
0xb2, 0xe5, 0x47, 0x66, 0xe3, 0xaf, 0x2c, 0xa4, 0x91, 0x8d, 0x4b, 0xc5, 0x70, 0x1e, 0x51,
0x19, 0x23, 0x7c, 0xab, 0x40, 0x24, 0x95, 0xef, 0xc8, 0x65, 0xdb, 0x5f, 0x0a, 0x41, 0x93,
0xff, 0x6c, 0x22, 0xb4, 0x9a, 0xe2, 0x20, 0xc1, 0x95, 0xa0, 0x3c, 0xc2, 0x13, 0xdb, 0xc8,
0x24, 0x33, 0x77, 0x75,
];
round_trip(&image, &sha512, 4);
}
+50 -8
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -7,22 +7,64 @@ use std::io::{self, Cursor};
use avbroot::{
self,
format::cpio::{CpioEntryType, CpioReader, CpioWriter},
format::cpio::{CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
util,
};
fn generate_archive() -> Vec<u8> {
let writer = Cursor::new(Vec::new());
let mut cpio_writer = CpioWriter::new(writer, false);
for entry in [
CpioEntry::new_symlink(b"symlink", b"target"),
CpioEntry::new_directory(b"directory", 0o755),
CpioEntry::new_file(b"file", 0o644, CpioEntryData::Data(b"foobar".to_vec())),
CpioEntry {
path: b"reserved".to_vec(),
data: CpioEntryData::Size(0),
inode: 12345,
file_type: CpioEntryType::Reserved,
file_mode: 0o4777,
uid: 12345678,
gid: 87654321,
nlink: 2,
mtime: 1700000000,
dev_maj: 2222,
dev_min: 3333,
rdev_maj: 4444,
rdev_min: 5555,
crc32: 0xfedcba09,
},
] {
cpio_writer.start_entry(&entry).unwrap();
}
let writer = cpio_writer.finish().unwrap();
let data = writer.into_inner();
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xb0, 0x51, 0xac, 0x28, 0x6f, 0x78, 0xe2, 0xe7, 0x45, 0xa0, 0x52, 0x7c, 0xff, 0x42,
0x30, 0x55, 0xbd, 0x64, 0x7d, 0x4e, 0xb8, 0xe6, 0x95, 0xe5, 0x9b, 0xd1, 0x13, 0xd6,
0x43, 0x0e, 0x32, 0xb2, 0x4e, 0x62, 0xa4, 0x55, 0x64, 0x48, 0xb7, 0x32, 0x26, 0x57,
0x75, 0x07, 0xf5, 0xa6, 0x0f, 0x18, 0xc3, 0x9e, 0x9f, 0x06, 0xdb, 0xa4, 0xf7, 0xeb,
0x5e, 0x8f, 0xce, 0xd0, 0x2b, 0x54, 0x39, 0x57
],
);
data
}
#[test]
fn round_trip_archive() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/archive.cpio",
));
let data = generate_archive();
assert_ne!(data.len() % 512, 0);
for pad_to_block_size in [false, true] {
println!("Pad to block size: {pad_to_block_size}");
let reader = Cursor::new(data);
let reader = Cursor::new(&data);
let mut cpio_reader = CpioReader::new(reader, false);
let writer = Cursor::new(Vec::new());
@@ -40,7 +82,7 @@ fn round_trip_archive() {
let new_data = writer.get_ref().as_slice();
if pad_to_block_size {
assert!(new_data.starts_with(data));
assert!(new_data.starts_with(&data));
assert!(util::is_zero(&new_data[data.len()..]));
assert_eq!(new_data.len() % 512, 0);
} else {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -8
View File
@@ -1,8 +1,6 @@
[advisories]
vulnerability = "deny"
unmaintained = "deny"
version = 2
yanked = "deny"
notice = "deny"
ignore = [
# https://rustsec.org/advisories/RUSTSEC-2023-0071
#
@@ -29,19 +27,18 @@ ignore = [
]
[licenses]
version = 2
include-dev = true
unlicensed = "deny"
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"GPL-3.0",
"ISC",
"MIT",
"OpenSSL",
"Unicode-DFS-2016",
]
copyleft = "allow"
default = "deny"
[[licenses.clarify]]
name = "ring"
@@ -65,13 +62,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/bzip2-rs",
"https://github.com/chenxiaolong/zip",
"https://github.com/jongiddy/bzip2-rs",
]
+3 -1
View File
@@ -18,8 +18,10 @@ ring = "0.17.0"
rsa = "0.9.6"
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
toml_edit = { version = "0.21.0", features = ["serde"] }
toml_edit = { version = "0.22.9", 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
+14 -14
View File
@@ -22,7 +22,7 @@ data.kernel = true
avb.signed = true
data.type = "boot"
data.version = "v4"
data.ramdisks = ["init"]
data.ramdisks = [["init", "first_stage"]]
[profile.pixel_v4_gki.partitions.system]
avb.signed = false
@@ -43,11 +43,11 @@ data.deps = ["system"]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = ["otacerts"]
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes]
original = "f9477a35e3b60a495e49431c61e3897f11775f453a6a9897ead568357c963618"
patched = "eb045799a514300727357a5ec471f9b04b276991daf4fd72f17f840b2a7dd1b8"
original = "24a0a62cc08b96563f4872aee2fdd4a84d1a977b55c326dd9d4ddd92a1d326ea"
patched = "29889670efea78bace221742b19e8ace88b67137fc2f46dcd9dbdf67e4e42267"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
@@ -77,11 +77,11 @@ data.deps = ["system"]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = ["init_and_otacerts", "dlkm"]
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes]
original = "021b4510bc244f5f686fbff89eb2058ec9c96a2949c2fe8caa7750a78d593225"
patched = "5d1a36e2eb18d9d905ab397d4eaf2d7e7c94da6e7573afe3d4e4367841171fe3"
original = "0e7d0924a68d46e00abe96abfa0e5f3a98d5d15a32bef7401b91fca9a19748e8"
patched = "1a2f53d9ac3a1da75e5e7502110bda437c5a725764f16656c564d42460136279"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
@@ -91,7 +91,7 @@ avb.signed = true
data.type = "boot"
data.version = "v3"
data.kernel = true
data.ramdisks = ["init"]
data.ramdisks = [["init"]]
[profile.pixel_v3.partitions.system]
avb.signed = false
@@ -112,11 +112,11 @@ data.deps = ["system"]
avb.signed = false
data.type = "boot"
data.version = "vendor_v3"
data.ramdisks = ["otacerts"]
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes]
original = "12221a69ff32e137d5f19b61f576fc6b33f0973c4a81da7722c640554ff4bc4e"
patched = "e2049c6eba6990fc5ce30130af470c134a5ccc42947dcdd398f737fbca7ae44a"
original = "533e6f233cb98c98c945044c2ee81a6069e66baee6f7dbcfbf7523795a11215e"
patched = "1eeae9dba0302c2d469bd03c8bccdc0469c171204dbd676a20cb6620d2d11c5c"
# Google Pixel 4a
# What's unique: boot (boot v2)
@@ -126,7 +126,7 @@ avb.signed = false
data.type = "boot"
data.version = "v2"
data.kernel = true
data.ramdisks = ["init_and_otacerts"]
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v2.partitions.system]
avb.signed = false
@@ -144,5 +144,5 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes]
original = "8b38d2d999b5b6e240e894f669e9e2643b3764c108d53bb7b02447da725e7c18"
patched = "16f56e3d02c08bb646d8d0694ce77a6edb02a613bbcff14b10ead4a448c3dc00"
original = "958dfa428abd2901178b90147903d7857ed78d6c016f6cb3af30d024a22a8f9a"
patched = "b0d18ef350ca7b6499b7de4b0182f4ac3be7288ad238ce888708643e750f7631"
+9
View File
@@ -5,6 +5,7 @@
use std::path::PathBuf;
use avbroot::cli::args::{LogFormat, LogLevel};
use clap::{Args, Parser, Subcommand};
#[derive(Debug, Args)]
@@ -66,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,
}
+7 -6
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -7,7 +7,7 @@ use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use toml_edit::Document;
use toml_edit::DocumentMut;
#[derive(Serialize, Deserialize)]
pub struct Sha256Hash(
@@ -41,7 +41,8 @@ pub struct Avb {
pub enum RamdiskContent {
Init,
Otacerts,
InitAndOtacerts,
FirstStage,
DsuKeyDir,
Dlkm,
}
@@ -62,7 +63,7 @@ pub struct BootData {
#[serde(default)]
pub kernel: bool,
#[serde(default)]
pub ramdisks: Vec<RamdiskContent>,
pub ramdisks: Vec<Vec<RamdiskContent>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -120,12 +121,12 @@ pub struct Config {
pub profile: BTreeMap<String, Profile>,
}
pub fn load_config(path: &Path) -> Result<(Config, Document)> {
pub fn load_config(path: &Path) -> Result<(Config, DocumentMut)> {
let contents =
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
let config: Config = toml_edit::de::from_str(&contents)
.with_context(|| format!("Failed to parse config: {path:?}"))?;
let document: Document = contents.parse().unwrap();
let document: DocumentMut = contents.parse().unwrap();
Ok((config, document))
}
+56 -34
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023 Pascal Roeleven
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -51,6 +51,7 @@ use clap::Parser;
use rsa::RsaPrivateKey;
use tempfile::{NamedTempFile, TempDir};
use topological_sort::TopologicalSort;
use tracing::{info, info_span};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
@@ -63,7 +64,7 @@ use crate::{
};
fn hash_file(path: &Path, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
println!("Calculating hash of {path:?}");
info!("Calculating hash: {path:?}");
let raw_reader =
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
@@ -195,7 +196,7 @@ fn append_avb(
// Give enough free space for changes from patching.
.max(1024 * 1024);
avb::write_appended_image(file, &header, &mut footer, full_image_size)?;
avb::write_appended_image(file, &header, &mut footer, Some(full_image_size))?;
Ok(())
}
@@ -226,6 +227,14 @@ fn ramdisk_add_otacerts(entries: &mut Vec<CpioEntry>, cert_ota: &Certificate) ->
Ok(())
}
fn ramdisk_add_first_stage(entries: &mut Vec<CpioEntry>) {
entries.push(CpioEntry::new_directory(b"first_stage_ramdisk", 0o755));
}
fn ramdisk_add_dsu_key_dir(entries: &mut Vec<CpioEntry>) {
entries.push(CpioEntry::new_directory(b"first_stage_ramdisk/avb", 0o755));
}
fn ramdisk_add_dlkm(entries: &mut Vec<CpioEntry>) {
for path in [b"lib".as_slice(), b"lib/modules".as_slice()] {
entries.push(CpioEntry::new_directory(path, 0o755));
@@ -244,25 +253,29 @@ fn ramdisk_add_dlkm(entries: &mut Vec<CpioEntry>) {
}
fn create_ramdisk(
content: RamdiskContent,
content_list: &[RamdiskContent],
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let mut entries = vec![];
match content {
RamdiskContent::Init => {
ramdisk_add_init(&mut entries);
}
RamdiskContent::Otacerts => {
ramdisk_add_otacerts(&mut entries, cert_ota)?;
}
RamdiskContent::InitAndOtacerts => {
ramdisk_add_init(&mut entries);
ramdisk_add_otacerts(&mut entries, cert_ota)?;
}
RamdiskContent::Dlkm => {
ramdisk_add_dlkm(&mut entries);
for content in content_list {
match content {
RamdiskContent::Init => {
ramdisk_add_init(&mut entries);
}
RamdiskContent::Otacerts => {
ramdisk_add_otacerts(&mut entries, cert_ota)?;
}
RamdiskContent::FirstStage => {
ramdisk_add_first_stage(&mut entries);
}
RamdiskContent::DsuKeyDir => {
ramdisk_add_dsu_key_dir(&mut entries);
}
RamdiskContent::Dlkm => {
ramdisk_add_dlkm(&mut entries);
}
}
}
@@ -297,7 +310,7 @@ fn create_boot_image(
let ramdisks = boot_data
.ramdisks
.iter()
.map(|c| create_ramdisk(*c, cert_ota, cancel_signal))
.map(|c| create_ramdisk(c, cert_ota, cancel_signal))
.collect::<Result<Vec<_>>>()?;
let boot_image = match boot_data.version {
@@ -360,17 +373,20 @@ fn create_boot_image(
ramdisk_metas: boot_data
.ramdisks
.iter()
.map(|c| match c {
RamdiskContent::Dlkm => RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
ramdisk_name: "dlkm".to_owned(),
board_id: Default::default(),
},
_ => RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_PLATFORM,
ramdisk_name: String::new(),
board_id: Default::default(),
},
.map(|c_list| {
if c_list.iter().any(|c| *c == RamdiskContent::Dlkm) {
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
ramdisk_name: "dlkm".to_owned(),
board_id: Default::default(),
}
} else {
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_PLATFORM,
ramdisk_name: String::new(),
board_id: Default::default(),
}
}
})
.collect(),
bootconfig: String::new(),
@@ -817,11 +833,12 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
"lib/x86_64/libmagiskinit.so",
] {
zip_writer.start_file(path, FileOptions::default())?;
write!(zip_writer, "dummy contents for {path}")?;
}
// avbroot looks for the version number in this file.
zip_writer.start_file("assets/util_functions.sh", FileOptions::default())?;
zip_writer.write_all(b"MAGISK_VER_CODE=26400\n")?;
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
Ok(())
}
@@ -947,7 +964,7 @@ fn patch_image(
keys: &KeySet,
cancel_signal: &AtomicBool,
) -> Result<()> {
println!("Patching {input_file:?}");
info!("Patching OTA: {input_file:?} -> {output_file:?}");
// We're intentionally using the CLI interface.
let mut args: Vec<&OsStr> = vec![
@@ -966,6 +983,7 @@ fn patch_image(
keys.ota_pass_file.path().as_os_str(),
OsStr::new("--cert-ota"),
keys.ota_cert_file.path().as_os_str(),
OsStr::new("--dsu"),
];
args.extend_from_slice(extra_args);
@@ -976,7 +994,7 @@ fn patch_image(
}
fn extract_image(input_file: &Path, output_dir: &Path, cancel_signal: &AtomicBool) -> Result<()> {
println!("Extracting AVB partitions from {input_file:?}");
info!("Extracting AVB partitions: {input_file:?} -> {output_dir:?}");
let cli = ExtractCli::try_parse_from([
OsStr::new("extract"),
@@ -991,7 +1009,7 @@ fn extract_image(input_file: &Path, output_dir: &Path, cancel_signal: &AtomicBoo
}
fn verify_image(input_file: &Path, keys: &KeySet, cancel_signal: &AtomicBool) -> Result<()> {
println!("Verifying signatures in {input_file:?}");
info!("Verifying signatures: {input_file:?}");
let cli = VerifyCli::try_parse_from([
OsStr::new("verify"),
@@ -1064,11 +1082,13 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
];
for name in profiles {
let _span = info_span!("profile", name).entered();
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe profile name: {name}");
}
println!("Generating OTA from profile: {name}");
info!("Generating OTA");
let profile = &config.profile[name];
@@ -1169,6 +1189,8 @@ fn main() -> Result<()> {
let cli = Cli::parse();
avbroot::cli::args::init_logging(cli.log_level, cli.log_format);
match cli.command {
Command::Test(c) => test_subcommand(&c, &cancel_signal),
Command::List(c) => list_subcommand(&c),
-33
View File
@@ -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
View File
@@ -1 +0,0 @@
#MAGISK
-83
View File
@@ -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);
}
}
}
-6
View File
@@ -1,6 +0,0 @@
id=com.chiller3.avbroot.oemunlockonboot
name=oemunlockonboot
version=v3.0.0
versionCode=196608
author=chenxiaolong
description=Enable OEM unlocking on every boot
-20
View File
@@ -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
View File
@@ -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.21.0"
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.22.9"
+1 -12
View File
@@ -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
View File
@@ -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,
}
-231
View File
@@ -1,231 +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_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::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 {
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,
}
+4 -55
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -11,7 +11,7 @@ use std::{
use anyhow::{bail, Result};
use clap::Parser;
use toml_edit::{value, Document};
use toml_edit::{value, DocumentMut};
use crate::WORKSPACE_DIR;
@@ -19,7 +19,7 @@ fn update_cargo_version(version: &str) -> Result<()> {
let path = Path::new(WORKSPACE_DIR).join("Cargo.toml");
let data = fs::read_to_string(&path)?;
let mut document: Document = data.parse()?;
let mut document: DocumentMut = data.parse()?;
document["workspace"]["package"]["version"] = value(version);
fs::write(path, document.to_string())?;
@@ -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.