mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
277 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59cf37faaf | |||
| 36269acd7b | |||
| 6aacc5a76c | |||
| bd4ebde403 | |||
| ffdae0bf88 | |||
| 8282f8087c | |||
| 6b11cd8af2 | |||
| f610f3b794 | |||
| b8d022d52c | |||
| cd38217111 | |||
| c8d548d224 | |||
| 9ad430ac7f | |||
| 8ca4eb5ad9 | |||
| e2b1ccb7a1 | |||
| 71a31ae01b | |||
| 83d7ffbc5e | |||
| e397998d9e | |||
| 8ef22508f5 | |||
| bf42a6a75c | |||
| a2fe6fc9d8 | |||
| faeb1fe988 | |||
| f1b2c6f468 | |||
| 2f964bf113 | |||
| f393d7adc4 | |||
| 72a1c3f216 | |||
| 2db8d3826e | |||
| 1448e55205 | |||
| b3862a9c4a | |||
| 088db04673 | |||
| b1410c869d | |||
| dd6eaf8e78 | |||
| 0eac8e6614 | |||
| 8a4f90176e | |||
| bd166594e2 | |||
| 19129ae927 | |||
| fb2aaed042 | |||
| 182d937d34 | |||
| 113bdec6dc | |||
| 1f0d012ac0 | |||
| e4fbe00ea2 | |||
| ad0b3d5aa8 | |||
| f4394b0c21 | |||
| 9dd98fd715 | |||
| b6e3c68241 | |||
| 6fee5346bb | |||
| 7d786150cf | |||
| b922f0d23c | |||
| 1f2b2170b3 | |||
| da124e4e05 | |||
| 7b778515d1 | |||
| 98745afe33 | |||
| a47c501211 | |||
| f2e47a65d4 | |||
| e11ddd2ba7 | |||
| 5d66774d13 | |||
| 3f09a506a0 | |||
| 4664f8ea37 | |||
| 796e2a4fa2 | |||
| e6b60d5d0f | |||
| e8cb4a8d53 | |||
| b2d280eb20 | |||
| e2dc5174b4 | |||
| 80c47e9a02 | |||
| 65ba3ad5cc | |||
| a7438876ce | |||
| c039901a85 | |||
| 0cdc7172dd | |||
| 1296275418 | |||
| a62d0a5c91 | |||
| de433a2724 | |||
| c08af33343 | |||
| e224884591 | |||
| a525fc4550 | |||
| 8c6b0fdfc5 | |||
| 36d4ed19ad | |||
| f9b06b33e6 | |||
| 620c873be5 | |||
| e929ecbe44 | |||
| 31685713ef | |||
| 0dab7aa32c | |||
| ba6c1b1400 | |||
| 1ecbf1144d | |||
| 84fa6c6bc6 | |||
| 15b7db4631 | |||
| 1d9c1574da | |||
| 43db728b9d | |||
| fbe9f629ab | |||
| 80d5f19223 | |||
| cff5ac6b2e | |||
| 2e3b5db9fe | |||
| 58a279f3a4 | |||
| 1484cd47c3 | |||
| 2ee38b716b | |||
| d4eb231dd4 | |||
| cf064e145d | |||
| 3b0c97a93d | |||
| 2d4f08f48b | |||
| a753304dff | |||
| 9480e2ffa4 | |||
| 140d0ddd8b | |||
| e0b114cf71 | |||
| 78449f686d | |||
| a2fb807803 | |||
| 6de5cb783a | |||
| 12d6f7f78c | |||
| 3c0a77df21 | |||
| d1b6bce227 | |||
| 10e0748d18 | |||
| 9cf63c7036 | |||
| 291b3c887b | |||
| 5d7eb13fbc | |||
| 05cd74719e | |||
| 7e0d5584d9 | |||
| b351d47f28 | |||
| 421b0a5207 | |||
| cad08a6a2f | |||
| 0a32dfaec3 | |||
| b03b2ade6a | |||
| c198646c7c | |||
| d815a78e9a | |||
| 99c5800f96 | |||
| c801fbb069 | |||
| 0b250086a3 | |||
| f6c6c8ab40 | |||
| 5c353a5f42 | |||
| ad932ed445 | |||
| d04d5451dc | |||
| 1d5db9f731 | |||
| 409b9298a5 | |||
| 516238d907 | |||
| 39d5fbf76d | |||
| 20f5bdacc7 | |||
| 5b8eefa867 | |||
| b2610fa0e9 | |||
| 8a5550a43c | |||
| e16f4c7fa7 | |||
| 983e6c40a5 | |||
| 8729854727 | |||
| 0d1beb7734 | |||
| 2cd0d69238 | |||
| c0e65264e5 | |||
| 37b15a2b6a | |||
| e4994fbe98 | |||
| cf1bacab30 | |||
| 4832121160 | |||
| c614e61744 | |||
| 9ebce1666c | |||
| fb34198ffc | |||
| 4a5eab4ba0 | |||
| 1e1818ad8f | |||
| 3f25ad7c76 | |||
| 37e28eb040 | |||
| ce87757fd1 | |||
| 10c425dede | |||
| 8a0d147993 | |||
| 062aa21485 | |||
| 7ffeb5e5cb | |||
| a9a1aa55e9 | |||
| 0ca2872111 | |||
| a403c26b54 | |||
| 4afa1d1ba6 | |||
| 9f669bc53e | |||
| fc89159f09 | |||
| 18f43a0e57 | |||
| af7262a0d2 | |||
| 5a7885674e | |||
| c974ab5ec3 | |||
| 02ae9cd0e3 | |||
| 9c92d32a80 | |||
| 296df25fb9 | |||
| 352352b4d4 | |||
| e28ef42c0e | |||
| 5775c86e5b | |||
| 3dc8e07c0f | |||
| 040dcd1a5c | |||
| 41a578975f | |||
| e9638d25b0 | |||
| e25080ddef | |||
| 0e7d778cf5 | |||
| 8ca1a289a8 | |||
| dfbc2f807f | |||
| e55cf3d679 | |||
| 7c38c5609b | |||
| 13c910274e | |||
| dd40f64918 | |||
| 7113fb32ef | |||
| 9c9d656ea2 | |||
| e06674a7f0 | |||
| cf5ef13e47 | |||
| 5fada419cb | |||
| eeea9f41b4 | |||
| 0bebf120c6 | |||
| 264c602fdb | |||
| 343e2e279c | |||
| 59ca759262 | |||
| 83ab475c11 | |||
| fe54640029 | |||
| fec1840a5f | |||
| 395f6934ff | |||
| a83b2fbfa9 | |||
| e18ef20e4d | |||
| b140620ed3 | |||
| dd9d8959fd | |||
| 9a7cece973 | |||
| aac3aded78 | |||
| 24320d4fae | |||
| 6800ae073e | |||
| 4d90ee2ac6 | |||
| 6b087c0844 | |||
| d384a8a99b | |||
| bc358c62af | |||
| fc05cb901a | |||
| 031ac8aa31 | |||
| 4a1dab4069 | |||
| bf885e40cf | |||
| c2a441cf78 | |||
| f6c6a9509a | |||
| 5a00995366 | |||
| edf537aad6 | |||
| f006f20209 | |||
| f36c1ca451 | |||
| 29b72961a3 | |||
| e105efb6d3 | |||
| 2a7df104ed | |||
| 8e52a9cf8c | |||
| f9343aa542 | |||
| 0391d4e2c3 | |||
| c18800dc44 | |||
| 2db90f83c2 | |||
| 34915e256f | |||
| 61392eb9d6 | |||
| 029cb4264e | |||
| 98a7fc811d | |||
| f4f4ec8e0d | |||
| 99b316f55e | |||
| 50ee90b61a | |||
| 2e8bd9f9d4 | |||
| 3fa96714c4 | |||
| 7a2530a199 | |||
| f84df86ef5 | |||
| 0c064981dd | |||
| 5645183ecc | |||
| bc7358a8d9 | |||
| d47c14ab12 | |||
| f479fe1a08 | |||
| df7b76bc59 | |||
| e342b93902 | |||
| d7439e15ae | |||
| 0f16f30dfb | |||
| 178c025eca | |||
| d6ac94c430 | |||
| 2de260a66c | |||
| e9ba770a15 | |||
| 3940b31acb | |||
| e87ee7c4ad | |||
| 0f8e30da37 | |||
| ae0863e319 | |||
| 46662796a0 | |||
| 237d4e9b11 | |||
| 5adff50b01 | |||
| 905eb9245c | |||
| e22f9f5676 | |||
| 17162df1e7 | |||
| b4ea4dfc9c | |||
| 5572493acf | |||
| 9d38748221 | |||
| fd0408dffe | |||
| af0fb1d9b4 | |||
| 8c965552e8 | |||
| 2532476583 | |||
| cf77999d95 | |||
| 0ade50f49e | |||
| 0bad6f8c6f | |||
| 4ce4863237 | |||
| f615bf48cc | |||
| b505b14574 | |||
| e2adff6eed |
+98
-39
@@ -13,24 +13,59 @@ 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
|
||||
- os: ubuntu-latest
|
||||
name: aarch64-linux-android31
|
||||
targets:
|
||||
- aarch64-linux-android
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
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.3
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
@@ -40,66 +75,90 @@ 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
|
||||
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
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,4 +1,3 @@
|
||||
---
|
||||
name: cargo-deny
|
||||
on:
|
||||
push:
|
||||
@@ -11,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
uses: EmbarkStudios/cargo-deny-action@30f817c6f72275c6d54dc744fbca09ebc958599f # v2.0.12
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Github Release
|
||||
on:
|
||||
push:
|
||||
@@ -25,14 +24,12 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
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
|
||||
|
||||
+308
@@ -7,6 +7,198 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.18.0
|
||||
|
||||
* Make OTA metadata property file field validation more lenient ([Issue #469], [PR #470])
|
||||
* Fixes `avbroot ota verify` for stock OTAs that include extra zip file entries in the metadata
|
||||
* Remove automatic promotion of insecure SHA-1 AVB hash algorithm to SHA-256 ([Issue #366], [Issue #469], [PR #473])
|
||||
* There are insecure devices that don't support SHA-256 and won't boot with it.
|
||||
* The original feature was a bandaid for OnePlus devices to make them a tiny bit more secure. They used SHA-256 for every partition except `system`. However, OnePlus no longer supports custom AVB keys anyway, so this feature is going away.
|
||||
* Add support for Magisk 30200 ([PR #474])
|
||||
* Update dependencies ([PR #475])
|
||||
|
||||
### Version 3.17.2
|
||||
|
||||
* Add support for Magisk 30100 ([PR #468])
|
||||
|
||||
### Version 3.17.1
|
||||
|
||||
* Update end-to-end tests to place streaming and seekable OTAs in separate directories for easier troubleshooting ([PR #463])
|
||||
* Update dependencies ([PR #464])
|
||||
* Add support for Magisk 30000 ([PR #467])
|
||||
|
||||
### Version 3.17.0
|
||||
|
||||
* Fix reserved space error when patching OTA zips larger than ~10 GB ([Issue #451], [PR #452])
|
||||
* Update dependencies ([PR #453])
|
||||
|
||||
### Version 3.16.1
|
||||
|
||||
* Add support for Magisk 29000 ([PR #448])
|
||||
* Update dependencies ([PR #449])
|
||||
|
||||
### Version 3.16.0
|
||||
|
||||
* Add support for CoW version 3 for virtual A/B ([Issue #441], [PR #442], [PR #445])
|
||||
* This was recently introduced with the Pixel 9a. Previous devices all used CoW version 2.
|
||||
* Add support for uncompressed CoW for virtual A/B ([PR #443])
|
||||
* This is not used on actual devices, but is very useful for testing the CoW estimation logic.
|
||||
* All differences between avbroot's and AOSP delta_generator's estimation logic are now fixed.
|
||||
* Add support for custom CoW compression levels for virtual A/B ([PR #444])
|
||||
* This is also not used on actual devices, but is supported by AOSP, so avbroot should support it too.
|
||||
* Update dependencies ([PR #446])
|
||||
|
||||
### Version 3.15.0
|
||||
|
||||
* Add support for changing the virtual A/B compression algorithm ([PR #437])
|
||||
* For devices that launched with Android <14, `--vabc-algo lz4` can significantly increase OTA installation speed when using a custom OTA updater app (with caveats). There is no difference when sideloading from recovery mode.
|
||||
* See [the documentation](./README.md#changing-virtual-ab-cow-compression-algorithm) for more details.
|
||||
* Switch back to the ring library now that it is maintained again ([PR #438])
|
||||
* Update dependencies ([PR #439])
|
||||
|
||||
### Version 3.14.0
|
||||
|
||||
* Report as many errors as possible before failing in `avbroot ota verify` and improve error messages ([Discussion #426], [PR #428], [PR #430])
|
||||
* Fix new clippy warnings introduced in Rust 1.85 ([PR #429])
|
||||
* Fix massive performance regression introduced in 3.13.0 for OTAs that use gzip for virtual A/B CoW compression ([Issue #433], [PR #434])
|
||||
* Update dependencies ([PR #435])
|
||||
|
||||
### Version 3.13.0
|
||||
|
||||
* Fix parsing Samsung `super.img` files in `avbroot lp` due to Samsung putting their own data structures in a region that's supposed to be filled with zeros ([PR #415])
|
||||
* Add advanced option to skip replacing the OTA certificate in the system image ([Discussion #417], [PR #418])
|
||||
* Switch to stable bzip2-rs release and use zlib-rs as the backend for flate2 ([PR #421])
|
||||
* Switch to the aws-lc cryptography library for SHA1 and SHA2 hashing ([PR #422])
|
||||
* The ring library is no longer maintained
|
||||
* Fix incorrect `Partitions aren't protected by AVB: system` warning when using `--skip-system-ota-cert` ([PR #423])
|
||||
* Discard unneeded temp file sooner when using `--skip-system-ota-cert` ([PR #424])
|
||||
* Make `avbroot lp`'s parser less strict so that it can load on-device `super` partitions ([PR #425])
|
||||
* The on-disk layout on virtual A/B devices violates some requirements stated in AOSP's documentation
|
||||
* Update dependencies ([PR #427])
|
||||
|
||||
### Version 3.12.0
|
||||
|
||||
* Add new `-p <name>` option to `avbroot ota extract` for extracting specific partitions ([PR #408])
|
||||
* Deprecate the `--boot-only` option in `avbroot ota extract` ([PR #408])
|
||||
* The option will remain indefinitely for backwards compatibility, but is hidden from `--help`
|
||||
* Add support for extracting the embedded OTA certificate and AVB public key in `avbroot ota extract` ([PR #409])
|
||||
* Rename `avbroot key extract-avb` to `avbroot key encode-avb` for consistency with `avbroot key decode-avb` ([PR #410])
|
||||
* The old syntax will remain supported indefinitely for backwards compatibility, but is hidden from `--help`
|
||||
* Update dependencies ([PR #411])
|
||||
|
||||
### Version 3.11.0
|
||||
|
||||
* Fix crash when ignoring warning about `--magisk-preinit-device` not being specified ([PR #394])
|
||||
* When using `--ignore-magisk-warnings`, assume that unsupported Magisk versions newer than the latest supported version are capable of all features ([Issue #393], [PR #395])
|
||||
* Update bzip2-rs and switch to the Rust backend ([PR #397], [PR #402])
|
||||
* Minor code cleanup for custom integer range type ([PR #398])
|
||||
* Improve errors to make them less ambiguous about what went wrong ([PR #401])
|
||||
* Fix bug where a vendor v4 boot image that was truncated in the bootconfig padding section would be accepted as valid ([PR #401])
|
||||
* Avoid performing many small I/O operations when reading and writing cpio archives ([PR #403])
|
||||
* Update dependencies ([PR #404])
|
||||
|
||||
### Version 3.10.0
|
||||
|
||||
* Switch to using zerocopy library for all binary file format parsers ([PR #384])
|
||||
* Update to latest AOSP protobuf schema for the `payload.bin` metadata file format ([PR #385])
|
||||
* Update dependencies and pin Github Actions actions to specific commits ([PR #386], [PR #392])
|
||||
* Improve error messages from file format parsers ([PR #390])
|
||||
* Add support for Magisk 28100 ([PR #391])
|
||||
|
||||
### Version 3.9.0
|
||||
|
||||
* Update all dependencies ([PR #368], [PR #377])
|
||||
* Add advanced option to skip replacing the OTA certificate in the recovery image ([Issue #366], [PR #367], [PR #371])
|
||||
* Improve error message when an incompatible RSA key is used for AVB signing ([Issue #366], [PR #369])
|
||||
* Fix clippy warnings ([PR #370])
|
||||
* Allow `avbroot ota verify` to verify OTAs that lack `META-INF/com/android/metadata.pb` ([Issue #366], [PR #373])
|
||||
* Allow `avbroot ota verify` to verify OTAs where the payload signature does not set `unpadded_signature_size` ([Issue #366], [PR #374])
|
||||
* Allow `avbroot sparse` to parse sparse images with unknown fields (matches AOSP implementation) ([PR #376])
|
||||
|
||||
### Version 3.8.0
|
||||
|
||||
* Add `avbroot avb digest` subcommand for computing the special vbmeta digest ([PR #363])
|
||||
* Update all dependencies ([PR #364])
|
||||
|
||||
### Version 3.7.1
|
||||
|
||||
* Add support for Magisk 28000 ([PR #362])
|
||||
|
||||
### Version 3.7.0
|
||||
|
||||
* Fix a nasty regression since version 2.0.0 where recovery mode's `otacerts.zip` modifications were lost when using `--prepatched` with Magisk on some older devices, like the Pixel 4a ([Issue #356], [PR #357])
|
||||
* This affected older devices without `vendor_boot` or `recovery` partitions.
|
||||
* **This caused sideloading patched OTA updates from recovery mode to break on the affected devices.** To fix the problem without wiping the device and starting fresh, please follow the [steps in the PR](https://github.com/chenxiaolong/avbroot/pull/357#issuecomment-2365343050).
|
||||
* Print a useful error message when trying to prompt for a passphrase without an interactive terminal ([PR #336])
|
||||
* Add a new `--zip-mode seekable` option to allow writing OTA zip files without data descriptors ([Issue #328], [PR #337])
|
||||
* Add new commands for packing and unpacking logical partition images (`super.img`) ([PR #342], [PR #343])
|
||||
* Add new commands for packing and unpacking Android sparse images ([PR #347])
|
||||
* Allow `avbroot payload repack` and `avbroot payload info` commands to read delta payloads ([PR #354])
|
||||
* Switch to passterm library for password prompts ([PR #355])
|
||||
|
||||
### Version 3.6.0
|
||||
|
||||
* Add support for gzip compression when computing CoW size estimates ([Issue #332], [PR #333])
|
||||
* This allows `--replace` to successfully replace dynamic partitions on legacy devices, like the Pixel 4a 5G
|
||||
* Minor code cleanup ([PR #334], [PR #335])
|
||||
|
||||
### Version 3.5.0
|
||||
|
||||
* Update all dependencies ([PR #329])
|
||||
* Add new unpack and pack commands for `payload.bin` files ([Issue #328], [PR #331])
|
||||
|
||||
### Version 3.4.1
|
||||
|
||||
* Update all dependencies ([PR #321])
|
||||
* Add support for Magisk 27006 ([PR #323])
|
||||
|
||||
### Version 3.4.0
|
||||
|
||||
* Fix (unreachable) minor error handling logic when attempting to use unsupported AVB signing algorithms ([PR #311])
|
||||
* Add support for performing signing operations with external programs ([Issue #310], [PR #312])
|
||||
* See the linked issue for an example of how to sign with a Yubikey.
|
||||
|
||||
### Version 3.3.0
|
||||
|
||||
* Recompute CoW size estimate when replacing dynamic partitions ([Issue #306], [PR #307])
|
||||
* Fixes out of space error when flashing a patched OTA that uses `--replace` to replace a dynamic partition (eg. `system`) with a larger or more incompressible image
|
||||
* Add `avbroot payload info` subcommand for inspecting `payload.bin` headers ([PR #309])
|
||||
|
||||
### 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])
|
||||
@@ -155,6 +347,10 @@ 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
|
||||
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
|
||||
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
|
||||
[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
|
||||
@@ -169,6 +365,22 @@ 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
|
||||
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
|
||||
[Issue #310]: https://github.com/chenxiaolong/avbroot/issues/310
|
||||
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
|
||||
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
|
||||
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
|
||||
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
|
||||
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
|
||||
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
|
||||
[Issue #441]: https://github.com/chenxiaolong/avbroot/issues/441
|
||||
[Issue #451]: https://github.com/chenxiaolong/avbroot/issues/451
|
||||
[Issue #469]: https://github.com/chenxiaolong/avbroot/issues/469
|
||||
[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
|
||||
@@ -242,5 +454,101 @@ Behind-the-scenes changes:
|
||||
[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
|
||||
[PR #307]: https://github.com/chenxiaolong/avbroot/pull/307
|
||||
[PR #309]: https://github.com/chenxiaolong/avbroot/pull/309
|
||||
[PR #311]: https://github.com/chenxiaolong/avbroot/pull/311
|
||||
[PR #312]: https://github.com/chenxiaolong/avbroot/pull/312
|
||||
[PR #321]: https://github.com/chenxiaolong/avbroot/pull/321
|
||||
[PR #323]: https://github.com/chenxiaolong/avbroot/pull/323
|
||||
[PR #329]: https://github.com/chenxiaolong/avbroot/pull/329
|
||||
[PR #331]: https://github.com/chenxiaolong/avbroot/pull/331
|
||||
[PR #333]: https://github.com/chenxiaolong/avbroot/pull/333
|
||||
[PR #334]: https://github.com/chenxiaolong/avbroot/pull/334
|
||||
[PR #335]: https://github.com/chenxiaolong/avbroot/pull/335
|
||||
[PR #336]: https://github.com/chenxiaolong/avbroot/pull/336
|
||||
[PR #337]: https://github.com/chenxiaolong/avbroot/pull/337
|
||||
[PR #342]: https://github.com/chenxiaolong/avbroot/pull/342
|
||||
[PR #343]: https://github.com/chenxiaolong/avbroot/pull/343
|
||||
[PR #347]: https://github.com/chenxiaolong/avbroot/pull/347
|
||||
[PR #354]: https://github.com/chenxiaolong/avbroot/pull/354
|
||||
[PR #355]: https://github.com/chenxiaolong/avbroot/pull/355
|
||||
[PR #357]: https://github.com/chenxiaolong/avbroot/pull/357
|
||||
[PR #362]: https://github.com/chenxiaolong/avbroot/pull/362
|
||||
[PR #363]: https://github.com/chenxiaolong/avbroot/pull/363
|
||||
[PR #364]: https://github.com/chenxiaolong/avbroot/pull/364
|
||||
[PR #367]: https://github.com/chenxiaolong/avbroot/pull/367
|
||||
[PR #368]: https://github.com/chenxiaolong/avbroot/pull/368
|
||||
[PR #369]: https://github.com/chenxiaolong/avbroot/pull/369
|
||||
[PR #370]: https://github.com/chenxiaolong/avbroot/pull/370
|
||||
[PR #371]: https://github.com/chenxiaolong/avbroot/pull/371
|
||||
[PR #373]: https://github.com/chenxiaolong/avbroot/pull/373
|
||||
[PR #374]: https://github.com/chenxiaolong/avbroot/pull/374
|
||||
[PR #376]: https://github.com/chenxiaolong/avbroot/pull/376
|
||||
[PR #377]: https://github.com/chenxiaolong/avbroot/pull/377
|
||||
[PR #384]: https://github.com/chenxiaolong/avbroot/pull/384
|
||||
[PR #385]: https://github.com/chenxiaolong/avbroot/pull/385
|
||||
[PR #386]: https://github.com/chenxiaolong/avbroot/pull/386
|
||||
[PR #390]: https://github.com/chenxiaolong/avbroot/pull/390
|
||||
[PR #391]: https://github.com/chenxiaolong/avbroot/pull/391
|
||||
[PR #392]: https://github.com/chenxiaolong/avbroot/pull/392
|
||||
[PR #394]: https://github.com/chenxiaolong/avbroot/pull/394
|
||||
[PR #395]: https://github.com/chenxiaolong/avbroot/pull/395
|
||||
[PR #397]: https://github.com/chenxiaolong/avbroot/pull/397
|
||||
[PR #398]: https://github.com/chenxiaolong/avbroot/pull/398
|
||||
[PR #401]: https://github.com/chenxiaolong/avbroot/pull/401
|
||||
[PR #402]: https://github.com/chenxiaolong/avbroot/pull/402
|
||||
[PR #403]: https://github.com/chenxiaolong/avbroot/pull/403
|
||||
[PR #404]: https://github.com/chenxiaolong/avbroot/pull/404
|
||||
[PR #408]: https://github.com/chenxiaolong/avbroot/pull/408
|
||||
[PR #409]: https://github.com/chenxiaolong/avbroot/pull/409
|
||||
[PR #410]: https://github.com/chenxiaolong/avbroot/pull/410
|
||||
[PR #411]: https://github.com/chenxiaolong/avbroot/pull/411
|
||||
[PR #415]: https://github.com/chenxiaolong/avbroot/pull/415
|
||||
[PR #418]: https://github.com/chenxiaolong/avbroot/pull/418
|
||||
[PR #421]: https://github.com/chenxiaolong/avbroot/pull/421
|
||||
[PR #422]: https://github.com/chenxiaolong/avbroot/pull/422
|
||||
[PR #423]: https://github.com/chenxiaolong/avbroot/pull/423
|
||||
[PR #424]: https://github.com/chenxiaolong/avbroot/pull/424
|
||||
[PR #425]: https://github.com/chenxiaolong/avbroot/pull/425
|
||||
[PR #427]: https://github.com/chenxiaolong/avbroot/pull/427
|
||||
[PR #428]: https://github.com/chenxiaolong/avbroot/pull/428
|
||||
[PR #429]: https://github.com/chenxiaolong/avbroot/pull/429
|
||||
[PR #430]: https://github.com/chenxiaolong/avbroot/pull/430
|
||||
[PR #434]: https://github.com/chenxiaolong/avbroot/pull/434
|
||||
[PR #435]: https://github.com/chenxiaolong/avbroot/pull/435
|
||||
[PR #437]: https://github.com/chenxiaolong/avbroot/pull/437
|
||||
[PR #438]: https://github.com/chenxiaolong/avbroot/pull/438
|
||||
[PR #439]: https://github.com/chenxiaolong/avbroot/pull/439
|
||||
[PR #442]: https://github.com/chenxiaolong/avbroot/pull/442
|
||||
[PR #443]: https://github.com/chenxiaolong/avbroot/pull/443
|
||||
[PR #444]: https://github.com/chenxiaolong/avbroot/pull/444
|
||||
[PR #445]: https://github.com/chenxiaolong/avbroot/pull/445
|
||||
[PR #446]: https://github.com/chenxiaolong/avbroot/pull/446
|
||||
[PR #448]: https://github.com/chenxiaolong/avbroot/pull/448
|
||||
[PR #449]: https://github.com/chenxiaolong/avbroot/pull/449
|
||||
[PR #452]: https://github.com/chenxiaolong/avbroot/pull/452
|
||||
[PR #453]: https://github.com/chenxiaolong/avbroot/pull/453
|
||||
[PR #463]: https://github.com/chenxiaolong/avbroot/pull/463
|
||||
[PR #464]: https://github.com/chenxiaolong/avbroot/pull/464
|
||||
[PR #467]: https://github.com/chenxiaolong/avbroot/pull/467
|
||||
[PR #468]: https://github.com/chenxiaolong/avbroot/pull/468
|
||||
[PR #470]: https://github.com/chenxiaolong/avbroot/pull/470
|
||||
[PR #473]: https://github.com/chenxiaolong/avbroot/pull/473
|
||||
[PR #474]: https://github.com/chenxiaolong/avbroot/pull/474
|
||||
[PR #475]: https://github.com/chenxiaolong/avbroot/pull/475
|
||||
|
||||
Generated
+707
-486
File diff suppressed because it is too large
Load Diff
+10
-2
@@ -4,7 +4,15 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.1.0"
|
||||
version = "3.18.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
missing_fields_in_debug = "warn"
|
||||
redundant_clone = "deny"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
+147
-1
@@ -30,7 +30,9 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
|
||||
* To force an image to be signed, use `--key <path> --force`.
|
||||
* To force an image to be unsigned, use `--force` without specifying `--key`.
|
||||
|
||||
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
|
||||
|
||||
@@ -62,6 +64,16 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
|
||||
|
||||
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
|
||||
|
||||
### Computing vbmeta digest
|
||||
|
||||
```bash
|
||||
avbroot avb digest -i <root vbmeta image>
|
||||
```
|
||||
|
||||
This subcommand computes the vbmeta digest, which is defined as the SHA256 digest of the root vbmeta partition's header, followed by the chained partitions' headers (if any) in the order that they are listed. Chained partitions more than one level deep are ignored.
|
||||
|
||||
This digest is equal to the value of the `ro.boot.vbmeta.digest` property or the `RootOfTrust.verifiedBootHash` hardware attestation field.
|
||||
|
||||
## `avbroot boot`
|
||||
|
||||
### Unpacking a boot image
|
||||
@@ -248,3 +260,137 @@ avbroot hash-tree verify -i <input data file> -H <input hash tree file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
|
||||
|
||||
## `avbroot lp`
|
||||
|
||||
This set of commands is for working with LP (logical partition) images. These are the containers for dynamically-allocated partitions, like `system`. All LP images are supported:
|
||||
|
||||
* Empty images: These are the `super_empty.img` images in the factory images for newer Google Pixel devices. They define the layout of the `super` partition, but don't contain any actual data. They also do not contain a backup copy of the metadata. As an optimization, `fastboot` can fill in the actual data during flashing to avoid needing to reboot to fastbootd mode.
|
||||
* Normal images backed by a single device: These are standalone `super.img` images and are how logical partitions are physically stored on disk in most newer devices. They contain a backup copy of all metadata as well as actual partition data.
|
||||
* Normal images backed by multiple devices: These are images split across multiple files/partitions and are used on devices where support for LP was retrofitted. For example, the LP setup on newer Android builds for the Google Pixel 3a XL reuse the legacy `system` and `vendor` partitions because there is no `super` partition. These are similar to the single-file LP setups, except that data can be stored across all of the LP images. However, the metadata is only stored on the first LP image.
|
||||
|
||||
### Unpacking an LP image
|
||||
|
||||
```bash
|
||||
avbroot lp unpack -i <input LP image> [-i <input LP image>]...
|
||||
```
|
||||
|
||||
This subcommand unpacks the LP metadata to `lp.toml` and the partition images to the `lp_images` directory (for normal images).
|
||||
|
||||
If there are multiple images, they must be specified in order. If the order is not known, run `avbroot lp info` on each of the images. The one that successfully parses is the first image and the `block_devices` field in the output specifies the full ordering.
|
||||
|
||||
An LP image can have multiple slots. If the LP image originated from a factory image or OTA, all slots are likely identical. If the LP image was dumped from a real device that installed OTA updates in the past, the slots may differ. If the slots are not identical, then the `--slot` option is required to specify which slot to unpack.
|
||||
|
||||
### Packing an LP image
|
||||
|
||||
```bash
|
||||
avbroot lp pack -o <output LP image> [-o <output LP image>]...
|
||||
```
|
||||
|
||||
This subcommand packs a new LP image from the `lp.toml` file and `lp_images` directory (for normal images). Any `.img` files in the `lp_images` directory that don't have a corresponding entry in `lp.toml` are silently ignored.
|
||||
|
||||
All metadata slots in the newly packed LP image will be identical.
|
||||
|
||||
### Repacking an LP image
|
||||
|
||||
```bash
|
||||
avbroot lp repack -i <input LP image> [-i <input LP image>]... -o <output LP image> [-o <output LP image>]...
|
||||
```
|
||||
|
||||
This subcommand is logically equivalent to `avbroot lp unpack` followed by `avbroot lp pack`, except more efficient. Instead of unpacking and packing all partition images, the raw data is directly copied from the old LP image to the new LP image.
|
||||
|
||||
When `--slot` is specified, this is useful for discarding unwanted metadata slots and the partition data exclusive to them.
|
||||
|
||||
### Showing LP image metadata
|
||||
|
||||
```bash
|
||||
avbroot lp info -i <first LP image>
|
||||
```
|
||||
|
||||
This subcommand shows the LP image metadata, including all metadata slots. If there are multiple images, only the first one is needed because it is the only one that stores the metadata.
|
||||
|
||||
## `avbroot payload`
|
||||
|
||||
This set of commands is for working with payload binary files (`payload.bin`). The `unpack` and `pack` commands can only work with full payloads because they require the complete data to be available, but the `repack` and `info` commands also work with delta payloads.
|
||||
|
||||
### Unpacking a payload binary
|
||||
|
||||
```bash
|
||||
avbroot payload unpack -i <input payload>
|
||||
```
|
||||
|
||||
This subcommand unpacks the payload header information to `payload.toml` and the partition images to the `payload_images` directory.
|
||||
|
||||
Only full payload binaries can be unpacked. Delta payload binaries from incremental OTAs are not supported.
|
||||
|
||||
### Packing a payload binary
|
||||
|
||||
```bash
|
||||
avbroot payload pack -o <output payload> --key <OTA private key>
|
||||
```
|
||||
|
||||
This subcommand packs a new payload binary from the `payload.toml` file and `payload_images` directory. Any `.img` files in the `payload_images` directory that don't have a corresponding entry in `payload.toml` are silently ignored.
|
||||
|
||||
Packing a payload binary requires compressing all of the partition images, which is very CPU intensive. If re-signing an existing payload binary without making any other modifications is all that's needed, use the `repack` subcommand instead.
|
||||
|
||||
### Repacking a payload binary
|
||||
|
||||
```bash
|
||||
avbroot payload repack -i <input payload> -o <output payload> --key <OTA private key>
|
||||
```
|
||||
|
||||
This subcommand is logically equivalent to `avbroot payload unpack` followed by `avbroot payload pack`, except significantly more efficient. Instead of decompressing and recompressing all partition images, the raw data is directly copied from the input payload binary.
|
||||
|
||||
This is useful for re-signing a payload binary without making any other changes.
|
||||
|
||||
### Showing payload header information
|
||||
|
||||
```bash
|
||||
avbroot payload info -i <payload>
|
||||
```
|
||||
|
||||
This subcommand shows all of the payload header fields (which will likely be extremely long).
|
||||
|
||||
## `avbroot sparse`
|
||||
|
||||
This set of commands is for working with Android sparse images. All features of the file format are supported, including hole chunks and CRC32 checksums.
|
||||
|
||||
### Unpacking a sparse image
|
||||
|
||||
```bash
|
||||
avbroot sparse unpack -o <input sparse image> -o <output raw image>
|
||||
```
|
||||
|
||||
This subcommand unpacks a sparse image to a raw image. If the sparse image contains CRC32 checksums, they will be validated during unpacking. If the sparse image contains holes, the output image will be created as a native sparse file.
|
||||
|
||||
Certain fastboot factory images may have multiple sparse images, like `super_1.img`, `super_2.img`, etc., where they all touch a disjoint set of regions on the same partition. These can be unpacked by running this subcommand for each sparse image and specifying the `--preserve` option along with using the same output file. This preserves the existing data in the output file when unpacking each sparse image.
|
||||
|
||||
### Packing a sparse image
|
||||
|
||||
```bash
|
||||
avbroot sparse pack -i <input raw image> -o <output sparse image>
|
||||
```
|
||||
|
||||
This subcommand packs a new sparse image from a raw image. The default block size is 4096 bytes, which can be changed with the `--block-size` option.
|
||||
|
||||
By default, this will pack the entire input file. However, on Linux, there is an optimization where all holes in the input file, if it is a native sparse file, will be stored as hole chunks instead of `0`-filled chunks in the output sparse image.
|
||||
|
||||
To pack a partial sparse image, such as those used in the special fastboot factory images mentioned above, pass in `--region <start> <end>`. This option can be specified multiple times to pack multiple regions.
|
||||
|
||||
Unlike AOSP's `img2simg` tool, which never writes CRC32 checksums, this subcommand will write checksums if the input file has no holes and the entire file is being packed.
|
||||
|
||||
### Repacking a sparse image
|
||||
|
||||
```bash
|
||||
avbroot sparse repack -i <input sparse image> -o <output sparse image>
|
||||
```
|
||||
|
||||
This subcommand is logically equivalent to `avbroot sparse unpack` followed by `avbroot sparse pack`, except more efficient. This is useful for roundtrip testing of avbroot's sparse file parser.
|
||||
|
||||
### Showing sparse image metadata
|
||||
|
||||
```bash
|
||||
avbroot sparse info -i <input sparse image>
|
||||
```
|
||||
|
||||
This subcommand shows the sparse image metadata, including the header and all chunks.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# avbroot
|
||||
|
||||
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.
|
||||
(This page is also available in: [Russian (Русский)](./README.ru.md).)
|
||||
|
||||
avbroot is a tool for modifying Android A/B OTA images reproducibly and re-signing them with custom keys. It also includes a [collection of subcommands](./README.extra.md) for packing and unpacking numerous Android image formats.
|
||||
|
||||
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.
|
||||
|
||||
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
|
||||
@@ -13,27 +13,31 @@ 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
|
||||
|
||||
avbroot applies the following patches to the partition images:
|
||||
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access if requested.
|
||||
|
||||
* 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
|
||||
|
||||
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
|
||||
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader while rooted.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
|
||||
|
||||
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
|
||||
|
||||
* Any operation that causes an improperly-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes 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). A couple ways an improperly-signed boot image could be flashed include:
|
||||
|
||||
* The `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
|
||||
|
||||
* The `Uninstall Magisk` feature in Magisk. If root access is no longer needed, Magisk **must** be removed by repatching the OTA with the `--rootless` option, 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.
|
||||
|
||||
@@ -47,6 +51,8 @@ avbroot applies the following patches to the partition images:
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
Skip this step if you're updating Android, Magisk, or KernelSU after you've performed an [initial setup](#initial-setup). [Updates](#updates) do not require signing keys since you have already generated them in the initial setup.
|
||||
|
||||
4. Patch the OTA zip. The base command is:
|
||||
|
||||
```bash
|
||||
@@ -115,7 +121,7 @@ When patching OTAs for multiple devices, generating unique keys for each device
|
||||
2. Convert the public key portion of the AVB signing key to the AVB public key metadata format. This is the format that the bootloader requires when setting the custom root of trust.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
|
||||
@@ -124,47 +130,76 @@ When patching OTAs for multiple devices, generating unique keys for each device
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
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.
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS#8-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.
|
||||
1. Make sure that the version of fastboot is 34 or newer. Older versions have bugs that prevent the `fastboot flashall` command (required later) from working properly.
|
||||
|
||||
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.
|
||||
```bash
|
||||
fastboot --version
|
||||
```
|
||||
|
||||
3. Extract the partition images from the patched OTA that are different from the original.
|
||||
2. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
|
||||
|
||||
```bash
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
3. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
|
||||
|
||||
4. Extract the partition images from the patched OTA that are different from the original.
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip.patched \
|
||||
--directory extracted
|
||||
--directory extracted \
|
||||
--fastboot
|
||||
```
|
||||
|
||||
4. Flash the partition images that were extracted.
|
||||
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
|
||||
|
||||
For each partition inside `extracted/`, except for `system`, run:
|
||||
5. Set the `ANDROID_PRODUCT_OUT` environment variable to the directory containing the extracted files.
|
||||
|
||||
For sh/bash/zsh (Linux, macOS, WSL):
|
||||
|
||||
```bash
|
||||
fastboot flash <partition> extracted/<partition>.img
|
||||
export ANDROID_PRODUCT_OUT=extracted
|
||||
```
|
||||
|
||||
Then, reboot into recovery's fastbootd mode and flash `system`:
|
||||
For PowerShell (Windows):
|
||||
|
||||
```bash
|
||||
fastboot reboot fastboot
|
||||
fastboot flash system extracted/system.img
|
||||
```powershell
|
||||
$env:ANDROID_PRODUCT_OUT = "extracted"
|
||||
```
|
||||
|
||||
5. Set up the custom AVB public key in the bootloader.
|
||||
For cmd (Windows):
|
||||
|
||||
```bat
|
||||
set ANDROID_PRODUCT_OUT=extracted
|
||||
```
|
||||
|
||||
6. Flash the partition images that were extracted.
|
||||
|
||||
```bash
|
||||
fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
7. 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
|
||||
```
|
||||
|
||||
6. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
|
||||
8. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
|
||||
|
||||
Install the Magisk or KernelSU app and run the following command:
|
||||
|
||||
@@ -178,27 +213,37 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
7. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
|
||||
9. 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 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).
|
||||
10. That's it! To update the OS, Magisk, or KernelSU see the [next section](#updates).
|
||||
|
||||
## Updates
|
||||
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way: by patching (or repatching) the OTA.
|
||||
|
||||
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
|
||||
1. Generate a new patched OTA by following the steps in the [usage section](#usage).
|
||||
|
||||
2. Follow the step in the [usage section](#usage) to patch the new OTA.
|
||||
2. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
|
||||
|
||||
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
|
||||
|
||||
4. Sideload the patched OTA with `adb sideload`.
|
||||
|
||||
5. That's it!
|
||||
5. Restart your phone. Note: the phone will likely take a long time to startup after an OS update (a few minutes in some cases).
|
||||
|
||||
**Warning**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
|
||||
|
||||
The status can be found by running `adb logcat -v color -s update_engine`. Alternatively, if [Custota](https://github.com/chenxiaolong/Custota) is installed (even if it's not configured to point to a custom OTA server), it will show a notification until the snapshot merge operation completes.
|
||||
|
||||
## Reverting to stock firmware
|
||||
|
||||
@@ -256,8 +301,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
--partition <name> # init_boot or boot, depending on device
|
||||
```
|
||||
|
||||
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
|
||||
@@ -344,6 +388,29 @@ Note that avbroot will validate that the prepatched image is compatible with the
|
||||
|
||||
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
|
||||
|
||||
### Skipping OTA certificate patches
|
||||
|
||||
avbroot can skip modifying `otacerts.zip` with the `--skip-system-ota-cert` and `--skip-recovery-ota-cert` options. **Do not use these unless you have a good reason to do so.**
|
||||
|
||||
When `--skip-system-ota-cert` is used, the OTA certificates in the `system` partition will not be modified. This prevents custom OTA updater apps from installing further patched OTAs while booted into Android.
|
||||
|
||||
When `--skip-recovery-ota-cert` is used, the OTA certificates in the `vendor_boot` or `recovery` partition will not be modified. **This prevents sideloading further patched OTAs from recovery mode.**
|
||||
|
||||
If `--skip-recovery-ota-cert` is used because the OTA certificate was already manually added to the boot image, then [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done. The verification process is only capable of checking the boot image's copy of the OTA certificates, not the system image's copy of them.
|
||||
|
||||
### Skipping all patches
|
||||
|
||||
To have avbroot make the absolute minimal changes:
|
||||
|
||||
* Specify `--skip-system-ota-cert`
|
||||
* Specify `--skip-recovery-ota-cert`
|
||||
* Specify `--rootless`
|
||||
* Omit `--dsu`
|
||||
|
||||
This will re-sign the `vbmeta` partition and the OTA with the custom keys, but leave all other partitions untouched.
|
||||
|
||||
**This should only be used for advanced troubleshooting.** Without the OTA certificate patches, the resulting OTA will not be able to install further updates.
|
||||
|
||||
### Replacing partitions
|
||||
|
||||
avbroot supports replacing entire partitions in the OTA, even partitions that are not boot images (eg. `vendor_dlkm`). A partition can be replaced by passing in `--replace <partition name> /path/to/partition.img`.
|
||||
@@ -352,6 +419,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:
|
||||
@@ -362,6 +435,18 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
|
||||
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
|
||||
|
||||
### Changing virtual A/B CoW compression algorithm
|
||||
|
||||
The virtual A/B CoW compression algorithm can be changed by passing in `--vabc-algo <algo>` with `gz` or `lz4`. OTAs normally use an algorithm that is compatible with the initial version of Android shipped on the device.
|
||||
|
||||
* Devices launching with Android 12 support `gz` and `brotli` (unsupported by avbroot)
|
||||
* Devices launching with Android 14 support `lz4`
|
||||
* Devices launching with Android 15 support `zstd` (unsupported by avbroot)
|
||||
|
||||
Picking a fast algorithm, like lz4, can speed up OTA installation significantly when installing via a custom OTA updater app. However, there is no performance difference when sideloading an OTA from recovery mode.
|
||||
|
||||
Note that the currently running version of Android must support the specified compression algorithm or else the OTA will fail to install. For example, trying to install an Android 14 OTA that uses lz4 CoW compression will fail if the running system is Android 13.
|
||||
|
||||
### Non-interactive use
|
||||
|
||||
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
|
||||
@@ -398,17 +483,99 @@ avbroot prompts for the private key passphrases interactively by default. To run
|
||||
|
||||
* Use unencrypted private keys. This is strongly discouraged.
|
||||
|
||||
### Extracting the entire OTA
|
||||
### Extracting an OTA
|
||||
|
||||
To extract all images contained within the OTA's `payload.bin`, run:
|
||||
To extract the partition images contained within an OTA's `payload.bin`, run:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
By default, this only extracts the images that could potentially be patched by avbroot. To extract all images, use the `--all` option. To extract specific images, use the `--partition <name>` option, which can be specified multiple times.
|
||||
|
||||
This command also supports extracting the embedded OTA certificate and AVB public key using the `--cert-ota` and `--public-key-avb` options. To extract only these components, pass in `--none` to skip extracting partition images.
|
||||
|
||||
### Zip write mode
|
||||
|
||||
By default, avbroot uses streaming writes for the output OTA during patching. This means it computes the sha256 digest for the digital signature as the file is being written. This mode causes the zip file to contain data descriptors, which is part of the zip standard and works on the vast majority of devices. However, some devices may have broken zip file parsers and fail to properly read OTA zip files containing data descriptors. If this is the case, pass in `--zip-mode seekable` when patching.
|
||||
|
||||
The seekable mode writes zip files without data descriptors, but as the name implies, requires seeking around the file instead of writing it sequentially. The sha256 digest for the digital signature is computed after the zip file has been fully written.
|
||||
|
||||
### Signing with an external program
|
||||
|
||||
avbroot supports delegating all RSA signing operations to an external program with the `--signing-helper` option. When using this option, the `--key-avb` and `--key-ota` options must be given a public key instead of a private key.
|
||||
|
||||
For each signing operation, avbroot will invoke the program with:
|
||||
|
||||
```bash
|
||||
<helper> <algorithm> <public key>
|
||||
```
|
||||
|
||||
The algorithm is one of `SHA{256,512}_RSA{2048,4096}` and the public key is what was passed to avbroot. The program can use the public key to find the corresponding private key (eg. on a hardware security module). avbroot will write a PKCS#1 v1.5 padded digest to `stdin` and the helper program is expected to perform a raw RSA signing operation and write the raw signature (octet string matching key size) to `stdout`.
|
||||
|
||||
By default, this behavior is compatible with the `--signing_helper` option in AOSP's avbtool. However, avbroot additionally extends the arguments to support non-interactive use. If `--pass-{avb,ota}-file` or `--pass-{avb,ota}-env-var` are used, then the helper program will be invoked with two additional arguments that point to the password file or environment variable.
|
||||
|
||||
```bash
|
||||
<helper> <algorithm> <public key> file <pass file>
|
||||
# or
|
||||
<helper> <algorithm> <public key> env <env file>
|
||||
```
|
||||
|
||||
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
|
||||
|
||||
### 16K page size developer option
|
||||
|
||||
On recent devices running Android 16 and newer, there may be an option in Android's developer options to switch to a 16K page size kernel. This will not work when running an avbroot-patched OS. The switch internally works by flashing incremental OTAs:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` to switch to the 16K page size kernel (requires the `boot` partition to be currently flashed with the 4K kernel)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` to switch to the 4K page size kernel (requires the `boot` partition to be currently flashed with the 16K kernel)
|
||||
|
||||
These `boot_otas` are unflashable when running an avbroot-patched OS because the `payload.bin` inside of them are signed by the OEM's key. These are also not proper OTA files. They don't contain any OTA metadata and the zip file itself is not signed. It's nothing more than a plain old zip file that stores a signed `payload.bin`.
|
||||
|
||||
There are no plans to add support for patching these `boot_otas`. It requires support for modifying filesystems and handling incremental OTAs, both of which are very non-trivial.
|
||||
|
||||
Folks who are determined to make this work anyway can try these manual steps to sign these `boot_otas` with your own key. Since the incremental OTAs are not being regenerated, the `boot` partition must be left unmodified when running `avbroot ota patch`.
|
||||
|
||||
1. Unpack `vendor.img` with avbroot and [afsr](https://github.com/chenxiaolong/afsr).
|
||||
|
||||
```bash
|
||||
avbroot avb unpack -i vendor.img
|
||||
afsr unpack -i raw.img
|
||||
```
|
||||
|
||||
2. Extract `payload.bin` from `boot_otas/boot_ota_16k.zip`.
|
||||
|
||||
3. Re-sign `payload.bin` with your OTA key.
|
||||
|
||||
```bash
|
||||
avbroot payload repack \
|
||||
-i payload.bin.orig \
|
||||
-o payload.bin \
|
||||
-k ota.key \
|
||||
--output-properties payload_properties.txt
|
||||
```
|
||||
|
||||
4. Create a new zip of `payload.bin` and `payload_properties.txt`. The files must be stored uncompressed (eg. with `zip -0`).
|
||||
|
||||
5. Repeat the procedure for `boot_otas/boot_ota_4k.zip`.
|
||||
|
||||
6. Repack `vendor.img` and sign it with your AVB key.
|
||||
|
||||
```bash
|
||||
afsr pack -o raw.img
|
||||
avbroot avb pack -o vendor.img -k avb.key --recompute-size
|
||||
```
|
||||
|
||||
7. Patch the (normal) OTA with:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--replace vendor <modified vendor> \
|
||||
<normal arguments...>
|
||||
```
|
||||
|
||||
## Building from source
|
||||
|
||||
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
|
||||
@@ -423,25 +590,19 @@ 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`.
|
||||
|
||||
### 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 android build --release --target aarch64-linux-android
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
First, save the public key to a file listing the keys to be trusted. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
|
||||
|
||||
```bash
|
||||
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
|
||||
```
|
||||
|
||||
Then, verify the signature of the zip file using the list of trusted keys.
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
|
||||
```
|
||||
|
||||
If the file is successfully verified, the output will be:
|
||||
|
||||
```
|
||||
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
|
||||
```
|
||||
To verify the digital signatures of the downloads, follow [the steps here](https://github.com/chenxiaolong/chenxiaolong/blob/master/VERIFY_SSH_SIGNATURES.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+596
@@ -0,0 +1,596 @@
|
||||
# avbroot
|
||||
|
||||
avbroot – это утилита для воспроизводимой модификации OTA-образов Android A/B-формата и их переподписания пользовательскими ключами. Она также включает в себя [набор подкоманд](./README.extra.md) для упаковки и распаковки образов Android различных форматов.
|
||||
|
||||
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддерживаются только устройства, использующие современную 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-доступа, если это запрашивается.
|
||||
|
||||
* `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` в приложении Magisk. Если вам больше не нужен root-доступ, Magisk **должен быть удален путем репатчинга OTA** с использованием параметра `--rootless`, но не через его приложение.
|
||||
|
||||
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв 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 encode-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 в кодировке PKCS#8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
|
||||
|
||||
Если вы потеряете ключ(-и) подписи AVB или OTA, вы больше не сможете подписывать новые OTA-архивы. Придется генерировать новые ключи подписи и разблокировать загрузчик (что приведет к стиранию всех данных). В таком случае возвращайтесь к инструкции в разделе [использования.](#использование)
|
||||
|
||||
## Первоначальная настройка
|
||||
|
||||
1. Убедитесь, что вы используете утилиту fastboot версии 34 или новее. Предыдущие версии содержат баги, что не позволяют команде `fastboot flashall` (которая понадобится по ходу инструкции) работать правильно.
|
||||
|
||||
```bash
|
||||
fastboot --version
|
||||
```
|
||||
|
||||
2. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
|
||||
|
||||
```bash
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
3. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
|
||||
|
||||
4. Извлекаем из пропатченного OTA модифицированные образы:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip.patched \
|
||||
--directory extracted \
|
||||
--fastboot
|
||||
```
|
||||
|
||||
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
|
||||
|
||||
5. Установите переменную окружения `ANDROID_PRODUCT_OUT`, указав директорию с извлеченными файлами.
|
||||
|
||||
Для sh/bash/zsh (Linux, macOS, WSL):
|
||||
|
||||
```bash
|
||||
export ANDROID_PRODUCT_OUT=extracted
|
||||
```
|
||||
|
||||
Для PowerShell (Windows):
|
||||
|
||||
```powershell
|
||||
$env:ANDROID_PRODUCT_OUT = "extracted"
|
||||
```
|
||||
|
||||
Для cmd (Командная строка или Терминал) (Windows):
|
||||
|
||||
```bat
|
||||
set ANDROID_PRODUCT_OUT=extracted
|
||||
```
|
||||
|
||||
6. Прошейте извлеченные образы разделов.
|
||||
|
||||
```bash
|
||||
fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
|
||||
|
||||
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
|
||||
|
||||
7. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
|
||||
|
||||
```bash
|
||||
fastboot reboot-bootloader
|
||||
fastboot erase avb_custom_key
|
||||
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
|
||||
```
|
||||
|
||||
8. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
|
||||
|
||||
Установите приложение Magisk или KernelSU и выполните следующую команду:
|
||||
|
||||
```bash
|
||||
adb shell su -c 'dmesg | grep libfs_avb'
|
||||
```
|
||||
|
||||
Если AVB работает корректно, будет выведено следующее сообщение:
|
||||
|
||||
```bash
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
9. Перезагрузитесь в 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) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
|
||||
|
||||
10. Готово! Установка последующих обновлений системы, 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 как из-под загруженной системы, так и при прошивке через 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
|
||||
--partition <название раздела> # init_boot или boot, в зависимости от устройства
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Пропуск патчинга сертификата OTA
|
||||
|
||||
Вы можете пропустить изменение otacerts.zip, используя аргументы `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.**
|
||||
|
||||
При использовании `--skip-system-ota-cert`, сертификаты OTA в образе `system` изменены не будут. Это не позволит сторонним приложениям для OTA-обновлений устанавливать будущие пропатченные OTA из-под загруженной системы.
|
||||
|
||||
При использовании `--skip-recovery-ota-cert`, сертификаты OTA в образах `vendor_boot` или `recovery` изменены не будут. **Это не позволит устанавливать будущие пропатченные OTA в режиме Recovery.**
|
||||
|
||||
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
|
||||
|
||||
### Пропуск всех патчей
|
||||
|
||||
Чтобы внести самый минимум изменений, укажите аргументы:
|
||||
|
||||
* `--skip-system-ota-cert`
|
||||
* `--skip-recovery-ota-cert`
|
||||
* `--rootless`
|
||||
* не используйте аргумент `--dsu`.
|
||||
|
||||
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и 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`.
|
||||
|
||||
### Изменение алгоритма CoW сжатия для вирутального A/B
|
||||
|
||||
Алгоритм CoW (copy-on-write) сжатия для виртуального A/B можно изменить, используя аргумент `--vabc-algo <алгоритм>`, указав `gz` или `lz4`. Как правило, по умолчанию OTA использует алгоритм, который совместим с изначальной версией Android, на которой поставлялось устройство.
|
||||
|
||||
* Девайсы, поставляемые с Android 12, поддерживают `gz` и `brotli` (последний не поддерживается avbroot)
|
||||
* Девайсы, поставляемые с Android 14, поддерживают `lz4`
|
||||
* Девайсы, поставляемые с Android 15, поддерживают `zstd` (не поддерживается avbroot)
|
||||
|
||||
Выбор быстрого алгоритма, такого как lz4, может значительно ускорить установку OTA из-под системы (при использованием стороннего приложения для OTA-обновлений). Однако, при установке OTA в режиме Recovery, разницы в скорости не будет.
|
||||
|
||||
Обратите внимание, что текущая используемая версия Android должна поддерживать выбранный алгоритм сжатия. В противном случае установка завершится ошибкой. Например, попытка установить OTA-обновление с Android 14, использующее алгоритм lz4, приведет к ошибке, если установка производится из-под Android 13.
|
||||
|
||||
### Использование в неинтерактивном режиме
|
||||
|
||||
По умолчанию 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
|
||||
```
|
||||
|
||||
По умолчанию извлекаются только те образы, которые потенциально могут быть пропатчены с помощью avbroot. Чтобы извлечь все образы, используйте опцию `--all`. Для извлечения конкретных образов используйте опцию `--partition <название раздела>`, которую можно указать несколько раз.
|
||||
|
||||
Эта команда также поддерживает извлечение встроенного сертификата OTA и публичного ключа AVB с помощью опций `--cert-ota` и `--public-key-avb`. Чтобы извлечь только эти компоненты, укажите аргумент `--none`, чтобы пропустить извлечение образов разделов.
|
||||
|
||||
### Режим записи ZIP
|
||||
|
||||
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
|
||||
|
||||
Режим seekable записывает ZIP-файлы без описателей данных, но, как следует из названия, требует перемещения по файлу, вместо последовательной записи. Дайджест sha256 для цифровой подписи вычисляется после того, как ZIP-файл был полностью записан.
|
||||
|
||||
### Подписание с использованием внешней программы
|
||||
|
||||
avbroot поддерживает делегирование всех операций подписания RSA внешней программе с помощью опции `--signing-helper`. При использовании этой опции, для `--key-avb` и `--key-ota` должен быть указан публичный ключ вместо приватного.
|
||||
|
||||
Для каждой операции подписания, avbroot будет вызывать программу с параметрами:
|
||||
|
||||
```bash
|
||||
<helper> <algorithm> <public key>
|
||||
```
|
||||
|
||||
Алгоритм (`<algorithm>`) — это один из `SHA{256,512}_RSA{2048,4096}`, а публичный ключ (`<public key>`) — это тот, что был передан в avbroot. Внешняя программа может использовать публичный ключ для поиска соответствующего приватного ключа (например, на аппаратном модуле безопасности). avbroot запишет дайджест, отформатированный по PKCS#1 v1.5, в `stdin`, а внешняя программа должна выполнить операцию сырого подписания RSA и записать сырую подпись (октетная строка, соответствующая размеру ключа) в `stdout`.
|
||||
|
||||
По умолчанию, это поведение совместимо с опцией `--signing_helper` в avbtool от AOSP. Однако avbroot дополнительно расширяет аргументы для поддержки неинтерактивного использования. Если используются опции `--pass-{avb,ota}-file` или `--pass-{avb,ota}-env-var`, то внешняя программа будет вызвана с двумя дополнительными аргументами, указывающими на файл пароля или переменную окружения.
|
||||
|
||||
```bash
|
||||
<helper> <algorithm> <public key> file <pass file>
|
||||
# или
|
||||
<helper> <algorithm> <public key> env <env file>
|
||||
```
|
||||
|
||||
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
|
||||
|
||||
### Размер страницы 16 КБ в настройках для разработчиков
|
||||
|
||||
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
|
||||
|
||||
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
|
||||
|
||||
Поддержка `boot_otas` не планируется. Это потребует реализации функционала для модификации ФС в инкрементальных OTA и их дальнейшей обработки, что сделать очень непросто.
|
||||
|
||||
Если вы всё же хотите завести эту функцию, можно попробовать вручную подписать файлы в `boot_otas` собственным ключом. Поскольку инкрементальные OTA не пересоздаются, раздел `boot` должен оставаться без изменений во время выполнения команды `avbroot ota patch`.
|
||||
|
||||
1. Распакуйте `vendor.img` с помощью avbroot и [afsr](https://github.com/chenxiaolong/afsr):
|
||||
```bash
|
||||
avbroot avb unpack -i vendor.img
|
||||
afsr unpack -i raw.img
|
||||
```
|
||||
|
||||
2. Извлеките `payload.bin` из `boot_otas/boot_ota_16k.zip`.
|
||||
|
||||
3. Переподпишите `payload.bin` вашим OTA-ключом:
|
||||
```bash
|
||||
avbroot payload repack \
|
||||
-i payload.bin.orig \
|
||||
-o payload.bin \
|
||||
-k ota.key \
|
||||
--output-properties payload_properties.txt
|
||||
```
|
||||
|
||||
4. Создайте новый zip, включающий `payload.bin` и `payload_properties.txt`. Файлы должны быть добавлены без сжатия (например, с помощью `zip -0`).
|
||||
|
||||
5. Повторите эту процедуру для `boot_otas/boot_ota_4k.zip`.
|
||||
|
||||
6. Соберите `vendor.img` обратно и подпишите его вашим AVB-ключом:
|
||||
```bash
|
||||
afsr pack -o raw.img
|
||||
avbroot avb pack -o vendor.img -k avb.key --recompute-size
|
||||
```
|
||||
|
||||
7. Пропатчите обычный OTA-архив с прошивкой, подменив `vendor` на модифицированный образ:
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--replace vendor <модифицированный vendor.img> \
|
||||
<дальше указываются аргументы, как при обычном патчинге>
|
||||
```
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Исполняемый файл будет записан в `target/release/avbroot`.
|
||||
|
||||
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
|
||||
|
||||
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
|
||||
|
||||
### Кросс-компиляция на Android
|
||||
|
||||
Чтобы использовать кросс-компиляцию на Android, установите [cargo-android](https://github.com/chenxiaolong/cargo-android) и воспользуйтесь оболочкой `cargo android`. Чтобы создать релизную сборку для aarch64, выполните:
|
||||
|
||||
```bash
|
||||
cargo android build --release --target aarch64-linux-android
|
||||
```
|
||||
|
||||
Возможно выполнение тестов, если хост работает под управлением Linux, установлен qemu-user-static, а исполняемый файл собран с `RUSTFLAGS=-C target-feature=+crt-static` и `--features static`.
|
||||
|
||||
## Проверка цифровых подписей
|
||||
|
||||
Чтобы проверить цифровые подписи, [следуйте этой инструкции.](https://github.com/chenxiaolong/chenxiaolong/blob/master/VERIFY_SSH_SIGNATURES.md)
|
||||
|
||||
## Вклад
|
||||
|
||||
Буду рад вашему вкладу в разработку! Однако я вряд ли приму изменения для поддержки устройств, которые ведут себя значительно иначе, чем устройства Pixel.
|
||||
|
||||
## Лицензия
|
||||
|
||||
avbroot распространяется по лицензии GPLv3. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
|
||||
|
||||
+32
-27
@@ -10,28 +10,34 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
base64 = "0.21.3"
|
||||
bitflags = "2.4.1"
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
byteorder = "1.4.3"
|
||||
bzip2 = "0.6.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"] }
|
||||
# We can't upgrade to 0.10.0 until x509-cert updates it too, since it's part of
|
||||
# the public API.
|
||||
const-oid = "0.9.5"
|
||||
crc32fast = "1.4.2"
|
||||
ctrlc = "3.4.0"
|
||||
flate2 = "1.0.27"
|
||||
dlv-list = "0.6.0"
|
||||
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.2.1"
|
||||
liblzma = "0.4.1"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
passterm = "2.0.3"
|
||||
phf = { version = "0.12.1", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
prost = "0.12.1"
|
||||
prost = "0.14.1"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
@@ -39,45 +45,44 @@ regex = { version = "1.9.4", default-features = false, features = ["perf", "std"
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
ring = "0.17.0"
|
||||
rpassword = "7.2.0"
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.7"
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "1.0.47"
|
||||
toml_edit = { version = "0.21.0", features = ["serde"] }
|
||||
thiserror = "2.0.3"
|
||||
toml_edit = { version = "0.23.3", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
zerocopy = { version = "0.8.10", features = ["std"] }
|
||||
zerocopy-derive = "0.8.5"
|
||||
|
||||
# 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
|
||||
[dependencies.bzip2]
|
||||
git = "https://github.com/jongiddy/bzip2-rs"
|
||||
rev = "2aefcb4d3634de1df226c73d93f758d65228bb8c"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
# https://github.com/zip-rs/zip2/pull/367
|
||||
# https://github.com/zip-rs/zip2/pull/368
|
||||
# For getting the data offset when writing new zip entries.
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
git = "https://github.com/chenxiaolong/zip2"
|
||||
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
|
||||
default-features = false
|
||||
features = ["deflate"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
|
||||
libc = "0.2.158"
|
||||
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.12.1"
|
||||
protox = "0.5.0"
|
||||
constcat = "0.6.0"
|
||||
prost-build = "0.14.1"
|
||||
protox = "0.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["bzip2/static", "liblzma/static"]
|
||||
static = ["liblzma/static"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+56
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{env, ffi::OsStr, fs, io, path::Path};
|
||||
|
||||
@@ -30,8 +28,62 @@ fn main() {
|
||||
|
||||
let file_descriptors = protox::compile(&protos, [&in_dir]).unwrap();
|
||||
|
||||
const CUE_AI: &str = ".chromeos_update_engine.ApexInfo";
|
||||
const CUE_DAM: &str = ".chromeos_update_engine.DeltaArchiveManifest";
|
||||
const CUE_DPG: &str = ".chromeos_update_engine.DynamicPartitionGroup";
|
||||
const CUE_DPM: &str = ".chromeos_update_engine.DynamicPartitionMetadata";
|
||||
const CUE_PU: &str = ".chromeos_update_engine.PartitionUpdate";
|
||||
const CUE_VABCFS: &str = ".chromeos_update_engine.VABCFeatureSet";
|
||||
|
||||
const DERIVE_SERDE: &str = "#[derive(serde::Deserialize, serde::Serialize)]";
|
||||
const SERDE_DEFAULT: &str = "#[serde(default)]";
|
||||
const SERDE_SKIP: &str = "#[serde(skip)]";
|
||||
const SERDE_SKIP_IF_VEC_EMPTY: &str = "#[serde(skip_serializing_if = \"Vec::is_empty\")]";
|
||||
|
||||
use constcat::concat as c;
|
||||
|
||||
prost_build::Config::new()
|
||||
.btree_map(["."])
|
||||
// Allow deserializing and serializing the types we care about.
|
||||
.type_attribute(CUE_AI, DERIVE_SERDE)
|
||||
.type_attribute(CUE_DAM, DERIVE_SERDE)
|
||||
.type_attribute(CUE_DPG, DERIVE_SERDE)
|
||||
.type_attribute(CUE_DPM, DERIVE_SERDE)
|
||||
.type_attribute(CUE_PU, DERIVE_SERDE)
|
||||
.type_attribute(CUE_VABCFS, DERIVE_SERDE)
|
||||
// Allow default-initializing all fields.
|
||||
.type_attribute(CUE_AI, SERDE_DEFAULT)
|
||||
.type_attribute(CUE_DAM, SERDE_DEFAULT)
|
||||
.type_attribute(CUE_DPG, SERDE_DEFAULT)
|
||||
.type_attribute(CUE_DPM, SERDE_DEFAULT)
|
||||
.type_attribute(CUE_PU, SERDE_DEFAULT)
|
||||
.type_attribute(CUE_VABCFS, SERDE_DEFAULT)
|
||||
// Don't serialize fields that define the structure of the payload
|
||||
// binary and that we recompute during packing.
|
||||
.field_attribute(c!(CUE_DAM, ".signatures_offset"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_DAM, ".signatures_size"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".operations"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".estimate_cow_size"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".old_partition_info"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".new_partition_info"), SERDE_SKIP)
|
||||
// Don't serialize AVB 1.0 fields.
|
||||
.field_attribute(c!(CUE_PU, ".hash_tree_data_extent"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".hash_tree_extent"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".hash_tree_algorithm"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".hash_tree_salt"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".fec_data_extent"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".fec_extent"), SERDE_SKIP)
|
||||
.field_attribute(c!(CUE_PU, ".fec_roots"), SERDE_SKIP)
|
||||
// Don't serialize fields for incremental OTAs.
|
||||
.field_attribute(c!(CUE_PU, ".merge_operations"), SERDE_SKIP)
|
||||
// Don't serialize fields for vendor-signed images, which update_engine
|
||||
// doesn't support anyway.
|
||||
.field_attribute(c!(CUE_PU, ".new_partition_signature"), SERDE_SKIP)
|
||||
// Don't serialize empty lists.
|
||||
.field_attribute(c!(CUE_DAM, ".apex_info"), SERDE_SKIP_IF_VEC_EMPTY)
|
||||
.field_attribute(c!(CUE_DAM, ".partitions"), SERDE_SKIP_IF_VEC_EMPTY)
|
||||
.field_attribute(c!(CUE_DPG, ".partition_names"), SERDE_SKIP_IF_VEC_EMPTY)
|
||||
.field_attribute(c!(CUE_DPM, ".groups"), SERDE_SKIP_IF_VEC_EMPTY)
|
||||
.compile_fds(file_descriptors)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -316,6 +316,10 @@ message PartitionUpdate {
|
||||
// as a hint. If set to 0, libsnapshot should use alternative
|
||||
// methods for estimating size.
|
||||
optional uint64 estimate_cow_size = 19;
|
||||
|
||||
// Information about the cow used by Cow Writer to specify
|
||||
// number of cow operations to be written
|
||||
optional uint64 estimate_op_count_max = 20;
|
||||
}
|
||||
|
||||
message DynamicPartitionGroup {
|
||||
@@ -368,6 +372,10 @@ message DynamicPartitionMetadata {
|
||||
|
||||
// A collection of knobs to tune Virtual AB Compression
|
||||
optional VABCFeatureSet vabc_feature_set = 6;
|
||||
|
||||
// Max bytes to be compressed at once during ota. Options: 4k, 8k, 16k, 32k,
|
||||
// 64k, 128k
|
||||
optional uint64 compression_factor = 7;
|
||||
}
|
||||
|
||||
// Definition has been duplicated from
|
||||
|
||||
+17
-43
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -12,10 +10,10 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing::{Level, debug};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -27,48 +25,21 @@ pub enum Command {
|
||||
Fec(fec::FecCli),
|
||||
HashTree(hashtree::HashTreeCli),
|
||||
Key(key::KeyCli),
|
||||
Lp(lp::LpCli),
|
||||
Ota(ota::OtaCli),
|
||||
Payload(payload::PayloadCli),
|
||||
Sparse(sparse::SparseCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
#[command(hide = true)]
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
#[command(hide = true)]
|
||||
Extract(ota::ExtractCli),
|
||||
/// (Deprecated: Use `avbroot boot magisk-info` instead.)
|
||||
#[command(hide = true)]
|
||||
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,
|
||||
@@ -95,8 +66,8 @@ pub struct Cli {
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
|
||||
pub log_level: LogLevel,
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
|
||||
pub log_level: Level,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
|
||||
@@ -123,11 +94,11 @@ impl FormatTime for ShortUptime {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
|
||||
pub fn init_logging(log_level: Level, 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());
|
||||
.with_max_level(log_level);
|
||||
|
||||
match log_format {
|
||||
LogFormat::Short => {
|
||||
@@ -162,7 +133,10 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
|
||||
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
|
||||
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Lp(c) => lp::lp_main(&c, cancel_signal),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
Command::Payload(c) => payload::payload_main(&c, cancel_signal),
|
||||
Command::Sparse(c) => sparse::sparse_main(&c, cancel_signal),
|
||||
// Deprecated aliases.
|
||||
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
|
||||
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
|
||||
|
||||
+201
-57
@@ -1,19 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cap_std::{
|
||||
ambient_authority,
|
||||
fs::{Dir, OpenOptions},
|
||||
@@ -22,15 +19,15 @@ 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 tracing::{Span, debug_span, info, warn};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile, Reopen},
|
||||
stream::{self, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter, check_cancel},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -57,16 +54,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")?;
|
||||
|
||||
@@ -92,20 +95,6 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Packing with insecure algorithms is intentionally not supported, so promote
|
||||
/// to a secure algorithm if needed.
|
||||
fn promote_insecure_hash_algorithm(algorithm: &str) -> &str {
|
||||
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
|
||||
const NEW_ALGORITHM: &str = "sha256";
|
||||
|
||||
if INSECURE_ALGORITHMS.contains(&algorithm) {
|
||||
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
|
||||
NEW_ALGORITHM
|
||||
} else {
|
||||
algorithm
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
|
||||
/// both readable and writable.
|
||||
fn write_raw(
|
||||
@@ -193,13 +182,11 @@ 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();
|
||||
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();
|
||||
d.image_size = image_size;
|
||||
raw_file.rewind()?;
|
||||
d.update(&mut raw_file, cancel_signal)
|
||||
@@ -338,7 +325,13 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
}
|
||||
|
||||
let originally_signed = !info.header.signature.is_empty();
|
||||
let mut sign_action = if originally_signed && &info.header != orig_header {
|
||||
let sign_action = if key_group.force {
|
||||
if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
}
|
||||
} else if originally_signed && &info.header != orig_header {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
// If the original image was signed, we can preserve the existing
|
||||
@@ -347,14 +340,6 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
SignAction::None
|
||||
};
|
||||
|
||||
if key_group.force {
|
||||
sign_action = if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
};
|
||||
}
|
||||
|
||||
match sign_action {
|
||||
SignAction::None => {
|
||||
if originally_signed {
|
||||
@@ -379,12 +364,28 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
key_group.pass_file.as_deref(),
|
||||
key_group.pass_env_var.as_deref(),
|
||||
);
|
||||
let private_key = crypto::read_pem_key_file(key_path, &source)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
let signing_key = if let Some(helper) = &key_group.signing_helper {
|
||||
let public_key = crypto::read_pem_public_key_file(key_path)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
|
||||
RsaSigningKey::External {
|
||||
program: helper.clone(),
|
||||
public_key_file: key_path.clone(),
|
||||
public_key,
|
||||
passphrase_source: source,
|
||||
}
|
||||
} else {
|
||||
let private_key = crypto::read_pem_key_file(key_path, &source)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
|
||||
RsaSigningKey::Internal(private_key)
|
||||
};
|
||||
|
||||
info.header.set_algo_for_key(&private_key)?;
|
||||
info.header
|
||||
.sign(&private_key)
|
||||
.set_algo_for_key(&signing_key)
|
||||
.context("Failed to set signature algorithm")?;
|
||||
info.header
|
||||
.sign(&signing_key)
|
||||
.context("Failed to sign new AVB header")?;
|
||||
}
|
||||
SignAction::Clear => {
|
||||
@@ -503,24 +504,21 @@ fn verify_and_repair(
|
||||
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(),
|
||||
};
|
||||
let suffix = name.map_or_else(String::new, |n| format!(" for: {n}"));
|
||||
|
||||
match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
info!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(&file, cancel_signal) {
|
||||
Err(e @ avb::Error::HashTree(_)) if repair => {
|
||||
Err(e @ avb::Error::HashTreeVerify(_)) if repair => {
|
||||
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warn!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(&file, cancel_signal).map(|_| {
|
||||
d.verify(&file, cancel_signal).inspect(|()| {
|
||||
info!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
@@ -584,6 +582,89 @@ pub fn verify_descriptors(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compute_digest_recursive(
|
||||
directory: &Dir,
|
||||
name: &str,
|
||||
context: &mut ring::digest::Context,
|
||||
max_depth: u8,
|
||||
seen: &mut HashSet<String>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
if max_depth == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
seen.insert(name.to_owned());
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let mut raw_reader = directory
|
||||
.open(&path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (header, footer, _) = avb::load_image(&mut raw_reader)
|
||||
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
|
||||
// We don't have a good way to get the length of the header, so we serialize
|
||||
// what we just parsed and compare it to the raw file so ensure that the
|
||||
// round-tripped data is identical.
|
||||
let raw_header = {
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
header
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to serialize header: {path:?}"))?;
|
||||
writer.into_inner()
|
||||
};
|
||||
|
||||
let header_offset = footer.map(|f| f.vbmeta_offset).unwrap_or_default();
|
||||
raw_reader
|
||||
.seek(SeekFrom::Start(header_offset))
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
let raw_header_orig = raw_reader
|
||||
.read_vec_exact(raw_header.len())
|
||||
.with_context(|| format!("Failed to reread AVB header: {path:?}"))?;
|
||||
|
||||
if raw_header != raw_header_orig {
|
||||
bail!("Serialized header does not match original header: {path:?}");
|
||||
}
|
||||
|
||||
context.update(&raw_header);
|
||||
|
||||
for descriptor in &header.descriptors {
|
||||
if let avb::Descriptor::ChainPartition(d) = descriptor {
|
||||
compute_digest_recursive(
|
||||
directory,
|
||||
&d.partition_name,
|
||||
context,
|
||||
max_depth - 1,
|
||||
seen,
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest. This is defined as the digest of the header in
|
||||
/// the root vbmeta image, followed by the headers in the immediate chained
|
||||
/// partitions. This digest is not defined to be recursive, so headers of
|
||||
/// chained partitions more than one level deep are ignored.
|
||||
pub fn compute_digest(directory: &Dir, name: &str, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
|
||||
compute_digest_recursive(directory, name, &mut context, 2, &mut seen, cancel_signal)?;
|
||||
|
||||
let digest = context.finish();
|
||||
|
||||
Ok(digest.as_ref().try_into().unwrap())
|
||||
}
|
||||
|
||||
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (info, mut reader) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
@@ -627,12 +708,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(())
|
||||
}
|
||||
|
||||
@@ -646,7 +731,6 @@ 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();
|
||||
d.update(&file, &file, None, cancel_signal)?;
|
||||
}
|
||||
|
||||
@@ -661,7 +745,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.
|
||||
@@ -716,6 +800,25 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let authority = ambient_authority();
|
||||
let parent_path = util::parent_path(&cli.input);
|
||||
let directory = Dir::open_ambient_dir(parent_path, authority)
|
||||
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
|
||||
let name = cli
|
||||
.input
|
||||
.file_stem()
|
||||
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
|
||||
|
||||
let digest = compute_digest(&directory, name, cancel_signal)?;
|
||||
|
||||
println!("{}", hex::encode(digest));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
|
||||
@@ -723,6 +826,7 @@ pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Info(c) => info_subcommand(c),
|
||||
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
AvbCommand::Digest(c) => digest_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,12 +839,15 @@ struct DisplayGroup {
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct KeyGroup {
|
||||
/// Path to private key for signing.
|
||||
/// Path to signing key.
|
||||
///
|
||||
/// A private key is needed if packing an image where the original header
|
||||
/// A signing key is needed if packing an image where the original header
|
||||
/// was signed and the header needs to be modified (eg. for a new checksum).
|
||||
/// If the header was originally not signed, then the private key is not
|
||||
/// If the header was originally not signed, then the signing key is not
|
||||
/// used, unless --force is specified.
|
||||
///
|
||||
/// This should normally be a private key. However, if --signing-helper is
|
||||
/// used, then it should be a public key instead.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
@@ -760,6 +867,15 @@ struct KeyGroup {
|
||||
/// File containing private key passphrase.
|
||||
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
|
||||
pass_file: Option<PathBuf>,
|
||||
|
||||
/// External program for signing.
|
||||
///
|
||||
/// If this option is specified, then --key must refer to a public key. The
|
||||
/// program will be invoked as:
|
||||
///
|
||||
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
|
||||
#[arg(long, value_name = "PROGRAM", value_parser)]
|
||||
signing_helper: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Unpack an AVB image.
|
||||
@@ -829,12 +945,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,
|
||||
|
||||
@@ -899,6 +1031,17 @@ struct VerifyCli {
|
||||
repair: bool,
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest.
|
||||
///
|
||||
/// This value is equal to what is reported by the ro.boot.vbmeta.digest
|
||||
/// property on a real device.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DigestCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum AvbCommand {
|
||||
Unpack(UnpackCli),
|
||||
@@ -907,6 +1050,7 @@ enum AvbCommand {
|
||||
#[command(alias = "dump")]
|
||||
Info(InfoCli),
|
||||
Verify(VerifyCli),
|
||||
Digest(DigestCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect AVB-protected images.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
@@ -9,7 +7,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
@@ -11,7 +9,7 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use bstr::ByteSlice;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -303,7 +301,7 @@ struct UnpackCli {
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output cpio info TOML.
|
||||
/// Path to output info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
|
||||
output_info: PathBuf,
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
|
||||
+16
-8
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
@@ -48,13 +46,16 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
crypto::write_pem_cert_file(&c.output, &cert)
|
||||
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::ExtractAvb(c) => {
|
||||
KeyCommand::ExtractAvb(c) | KeyCommand::EncodeAvb(c) => {
|
||||
let public_key = if let Some(p) = &c.input.key {
|
||||
let passphrase = get_passphrase_source(&c.passphrase, p);
|
||||
let private_key = crypto::read_pem_key_file(p, &passphrase)
|
||||
.with_context(|| format!("Failed to load key: {p:?}"))?;
|
||||
|
||||
private_key.to_public_key()
|
||||
} else if let Some(p) = &c.input.public_key {
|
||||
crypto::read_pem_public_key_file(p)
|
||||
.with_context(|| format!("Failed to load public key: {p:?}"))?
|
||||
} else if let Some(p) = &c.input.cert {
|
||||
let certificate = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
@@ -93,6 +94,10 @@ struct PublicKeyInputGroup {
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
/// Path to public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
|
||||
public_key: Option<PathBuf>,
|
||||
|
||||
/// Path to certificate.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
|
||||
cert: Option<PathBuf>,
|
||||
@@ -145,12 +150,12 @@ struct GenerateCertCli {
|
||||
validity: u64,
|
||||
}
|
||||
|
||||
/// Extract the AVB public key from a private key or certificate.
|
||||
/// Convert a key or certificate to an AVB-encoded public key.
|
||||
///
|
||||
/// The public key is stored in both the private key and the certificate. Either
|
||||
/// one can be used interchangeably.
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExtractAvbCli {
|
||||
struct EncodeAvbCli {
|
||||
/// Path to output AVB public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
@@ -178,7 +183,10 @@ struct DecodeAvbCli {
|
||||
enum KeyCommand {
|
||||
GenerateKey(GenerateKeyCli),
|
||||
GenerateCert(GenerateCertCli),
|
||||
ExtractAvb(ExtractAvbCli),
|
||||
/// (Deprecated: Use `avbroot key encode-avb` instead.)
|
||||
#[command(hide = true)]
|
||||
ExtractAvb(EncodeAvbCli),
|
||||
EncodeAvb(EncodeAvbCli),
|
||||
DecodeAvb(DecodeAvbCli),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use rayon::iter::{
|
||||
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE},
|
||||
stream::{self, FromReader, PSeekFile, Reopen, ToWriter},
|
||||
};
|
||||
|
||||
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<PSeekFile>, Metadata)> {
|
||||
let mut inputs = paths
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let p = p.as_ref();
|
||||
|
||||
File::open(p)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let metadata = Metadata::from_reader(&mut inputs[0])
|
||||
.with_context(|| format!("Failed to parse LP image metadata: {:?}", paths[0].as_ref()))?;
|
||||
|
||||
Ok((inputs, metadata))
|
||||
}
|
||||
|
||||
fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<PSeekFile>> {
|
||||
paths
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let p = p.as_ref();
|
||||
|
||||
File::create(p)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open LP image for writing: {p:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
}
|
||||
|
||||
fn read_info(path: &Path) -> Result<Metadata> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read metadata info TOML: {path:?}"))?;
|
||||
let info = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse metadata info TOML: {path:?}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn write_info(path: &Path, metadata: &Metadata) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(metadata)
|
||||
.with_context(|| format!("Failed to serialize metadata info TOML: {path:?}"))?;
|
||||
fs::write(path, data)
|
||||
.with_context(|| format!("Failed to write metadata info TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_metadata(cli: &LpCli, metadata: &Metadata) {
|
||||
if !cli.quiet {
|
||||
println!("{metadata:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
struct CopyExtent {
|
||||
device_index: usize,
|
||||
lp_offset: u64,
|
||||
out_offset: u64,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
/// Split extents into smaller ones for parallelization.
|
||||
fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
|
||||
// 64 MiB is the smallest size we'll parallelize by.
|
||||
const CHUNK_SIZE: u64 = 64 * 1024 * 1024;
|
||||
|
||||
let mut result = vec![];
|
||||
let mut out_offset = 0;
|
||||
|
||||
for extent in extents {
|
||||
let mut remain = extent.num_sectors * u64::from(SECTOR_SIZE);
|
||||
|
||||
match extent.extent_type {
|
||||
ExtentType::Linear {
|
||||
start_sector,
|
||||
block_device_index,
|
||||
} => {
|
||||
let mut lp_offset = start_sector * u64::from(SECTOR_SIZE);
|
||||
|
||||
// 64 MiB is the smallest size we'll parallelize by.
|
||||
let num_chunks = remain.div_ceil(64 * 1024 * 1024);
|
||||
|
||||
for _ in 0..num_chunks {
|
||||
let chunk_size = CHUNK_SIZE.min(remain);
|
||||
|
||||
result.push(CopyExtent {
|
||||
device_index: block_device_index,
|
||||
out_offset,
|
||||
lp_offset,
|
||||
size: chunk_size,
|
||||
});
|
||||
|
||||
out_offset += chunk_size;
|
||||
lp_offset += chunk_size;
|
||||
remain -= chunk_size;
|
||||
}
|
||||
}
|
||||
ExtentType::Zero => out_offset += remain,
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Use the CLI-specified slot or automatically select one if all slots are
|
||||
/// identical.
|
||||
fn get_slot_number(metadata: &Metadata, cli_slot: Option<u32>) -> Result<usize> {
|
||||
if let Some(n) = cli_slot {
|
||||
let n = n as usize;
|
||||
if n >= metadata.slots.len() {
|
||||
bail!("Slot out of range: {n}");
|
||||
}
|
||||
|
||||
Ok(n)
|
||||
} else {
|
||||
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
|
||||
bail!("A slot must be specified because they are not all identical");
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all slots aside from the specified one and return the old slot count.
|
||||
fn retain_slot(metadata: &mut Metadata, slot: usize) -> usize {
|
||||
let slot_count = metadata.slots.len();
|
||||
metadata.slots.swap(0, slot);
|
||||
metadata.slots.truncate(1);
|
||||
slot_count
|
||||
}
|
||||
|
||||
/// Duplicate the first slot until the required number of slots is reached.
|
||||
fn fill_slots(metadata: &mut Metadata) {
|
||||
let required = match metadata.image_type {
|
||||
ImageType::Normal => metadata.metadata_slot_count as usize,
|
||||
ImageType::Empty => 1,
|
||||
};
|
||||
|
||||
for _ in metadata.slots.len()..required {
|
||||
metadata.slots.extend_from_within(0..=0);
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut inputs = cli
|
||||
.input
|
||||
.iter()
|
||||
.map(|p| {
|
||||
File::open(p)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let mut metadata = Metadata::from_reader(&mut inputs[0])
|
||||
.with_context(|| format!("Failed to read LP image metadata: {:?}", cli.input[0]))?;
|
||||
|
||||
// Display and write only the selected slot.
|
||||
let slot_number = get_slot_number(&metadata, cli.slot)?;
|
||||
retain_slot(&mut metadata, slot_number);
|
||||
display_metadata(lp_cli, &metadata);
|
||||
write_info(&cli.output_info, &metadata)?;
|
||||
|
||||
// For empty images, there's no data to unpack.
|
||||
if metadata.image_type == ImageType::Empty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
|
||||
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
|
||||
|
||||
let slot = &metadata.slots[0];
|
||||
|
||||
if slot.block_devices.len() != inputs.len() {
|
||||
bail!(
|
||||
"Need {} input images, but have {}",
|
||||
slot.block_devices.len(),
|
||||
inputs.len(),
|
||||
);
|
||||
}
|
||||
|
||||
// Preopen all image output files.
|
||||
let mut paths = vec![];
|
||||
let mut files = vec![];
|
||||
|
||||
for group in &slot.groups {
|
||||
let mut group_paths = vec![];
|
||||
let mut group_files = vec![];
|
||||
|
||||
for partition in &group.partitions {
|
||||
// A partition name with unsafe characters fails during parsing.
|
||||
let path = format!("{}.img", partition.name);
|
||||
|
||||
let file = directory
|
||||
.create(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
|
||||
file.set_len(partition.size()?)
|
||||
.with_context(|| format!("Failed to truncate file: {path:?}"))?;
|
||||
|
||||
group_paths.push(path);
|
||||
group_files.push(file);
|
||||
}
|
||||
|
||||
paths.push(group_paths);
|
||||
files.push(group_files);
|
||||
}
|
||||
|
||||
slot.groups
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
// Flatten grouped partitions.
|
||||
.flat_map(|(g_index, g)| {
|
||||
g.partitions
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(move |(p_index, p)| (g_index, p_index, p))
|
||||
})
|
||||
// Flatten extents in all partitions and split them to smaller chunks
|
||||
// for better parallelism.
|
||||
.flat_map(|(g_index, p_index, p)| {
|
||||
split_extents(&p.extents)
|
||||
.into_par_iter()
|
||||
.map(move |e| (g_index, p_index, e))
|
||||
})
|
||||
.map(|(g_index, p_index, extent)| {
|
||||
// Never fails for PSeekFiles.
|
||||
let mut reader = inputs[extent.device_index].reopen()?;
|
||||
let mut writer = files[g_index][p_index].reopen()?;
|
||||
|
||||
let r_path = &cli.input[extent.device_index];
|
||||
let w_path = &paths[g_index][p_index];
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(extent.lp_offset))
|
||||
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(extent.out_offset))
|
||||
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut metadata = read_info(&cli.input_info)?;
|
||||
|
||||
if metadata.slots.len() != 1 {
|
||||
bail!("There must be exactly one metadata slot");
|
||||
}
|
||||
|
||||
let slot = &mut metadata.slots[0];
|
||||
|
||||
let mut outputs = open_lp_outputs(&cli.output)?;
|
||||
|
||||
if slot.block_devices.len() != outputs.len() {
|
||||
bail!(
|
||||
"Need {} output images, but have {}",
|
||||
slot.block_devices.len(),
|
||||
outputs.len(),
|
||||
);
|
||||
}
|
||||
|
||||
if metadata.image_type == ImageType::Normal {
|
||||
for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() {
|
||||
output
|
||||
.set_len(block_device.size)
|
||||
.with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?;
|
||||
}
|
||||
}
|
||||
|
||||
for group in &slot.groups {
|
||||
for partition in &group.partitions {
|
||||
let name = &partition.name;
|
||||
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preopen all image input files.
|
||||
let mut paths = vec![];
|
||||
let mut files = vec![];
|
||||
|
||||
if metadata.image_type == ImageType::Normal {
|
||||
let authority = ambient_authority();
|
||||
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
|
||||
|
||||
for group in &mut slot.groups {
|
||||
let mut group_paths = vec![];
|
||||
let mut group_files = vec![];
|
||||
|
||||
for partition in &mut group.partitions {
|
||||
let path = format!("{}.img", partition.name);
|
||||
|
||||
let mut file = directory
|
||||
.open(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
|
||||
let size = file
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
if size % u64::from(SECTOR_SIZE) != 0 {
|
||||
bail!("File size is not {SECTOR_SIZE}B aligned: {size}: {path:?}");
|
||||
}
|
||||
|
||||
// This will be filled out properly later during reallocation.
|
||||
partition.extents.push(Extent {
|
||||
num_sectors: size / u64::from(SECTOR_SIZE),
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 0,
|
||||
block_device_index: 0,
|
||||
},
|
||||
});
|
||||
|
||||
group_paths.push(path);
|
||||
group_files.push(file);
|
||||
}
|
||||
|
||||
paths.push(group_paths);
|
||||
files.push(group_files);
|
||||
}
|
||||
|
||||
// Now that we have all the partition sizes, actually allocate extents
|
||||
// for them on the block devices.
|
||||
slot.reallocate_extents()
|
||||
.context("Failed to allocate extents")?;
|
||||
}
|
||||
|
||||
// Display only the selected slot and make the rest identical.
|
||||
let _ = slot;
|
||||
display_metadata(lp_cli, &metadata);
|
||||
fill_slots(&mut metadata);
|
||||
let slot = &metadata.slots[0];
|
||||
|
||||
// Write the new metadata.
|
||||
metadata
|
||||
.to_writer(&mut outputs[0])
|
||||
.with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?;
|
||||
|
||||
// For empty images, there's no data to pack.
|
||||
if metadata.image_type == ImageType::Empty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
slot.groups
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
// Flatten grouped partitions.
|
||||
.flat_map(|(g_index, g)| {
|
||||
g.partitions
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(move |(p_index, p)| (g_index, p_index, p))
|
||||
})
|
||||
// Flatten extents in all partitions and split them to smaller chunks
|
||||
// for better parallelism.
|
||||
.flat_map(|(g_index, p_index, p)| {
|
||||
split_extents(&p.extents)
|
||||
.into_par_iter()
|
||||
.map(move |e| (g_index, p_index, e))
|
||||
})
|
||||
.map(|(g_index, p_index, extent)| {
|
||||
// Never fails for PSeekFiles.
|
||||
let mut reader = files[g_index][p_index].reopen()?;
|
||||
let mut writer = outputs[extent.device_index].reopen()?;
|
||||
|
||||
let r_path = &paths[g_index][p_index];
|
||||
let w_path = &cli.output[extent.device_index];
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(extent.out_offset))
|
||||
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(extent.lp_offset))
|
||||
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
// Show a clap-style error if the number of inputs and outputs aren't equal.
|
||||
if cli.input.len() != cli.output.len() {
|
||||
let (arg_id, actual_len, expected_len) = if cli.input.len() < cli.output.len() {
|
||||
("input", cli.input.len(), cli.output.len())
|
||||
} else {
|
||||
("output", cli.output.len(), cli.input.len())
|
||||
};
|
||||
|
||||
let mut command = RepackCli::command();
|
||||
command.build();
|
||||
|
||||
let arg = command
|
||||
.get_arguments()
|
||||
.find(|a| a.get_id() == arg_id)
|
||||
.expect("argument not found");
|
||||
|
||||
let mut error =
|
||||
clap::Error::new(clap::error::ErrorKind::WrongNumberOfValues).with_cmd(&command);
|
||||
error.insert(
|
||||
clap::error::ContextKind::InvalidArg,
|
||||
clap::error::ContextValue::String(arg.to_string()),
|
||||
);
|
||||
error.insert(
|
||||
clap::error::ContextKind::ActualNumValues,
|
||||
clap::error::ContextValue::Number(actual_len as isize),
|
||||
);
|
||||
error.insert(
|
||||
clap::error::ContextKind::ExpectedNumValues,
|
||||
clap::error::ContextValue::Number(expected_len as isize),
|
||||
);
|
||||
|
||||
// We don't show the usage because only Command::_build_subcommand() can
|
||||
// create an appropriate Command instance for showing the subcommand
|
||||
// usage and there's no way to call that, directly or indirectly.
|
||||
|
||||
error.exit();
|
||||
}
|
||||
|
||||
let (inputs, mut metadata) = open_lp_inputs(&cli.input)?;
|
||||
let mut outputs = open_lp_outputs(&cli.output)?;
|
||||
|
||||
// Display only the selected slot and make the rest identical.
|
||||
let slot_number = get_slot_number(&metadata, cli.slot)?;
|
||||
retain_slot(&mut metadata, slot_number);
|
||||
display_metadata(lp_cli, &metadata);
|
||||
fill_slots(&mut metadata);
|
||||
|
||||
let slot = &metadata.slots[0];
|
||||
|
||||
if slot.block_devices.len() != inputs.len() {
|
||||
bail!(
|
||||
"Need {} images, but have {}",
|
||||
slot.block_devices.len(),
|
||||
inputs.len(),
|
||||
);
|
||||
}
|
||||
|
||||
// Write the new metadata.
|
||||
metadata
|
||||
.to_writer(&mut outputs[0])
|
||||
.with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?;
|
||||
|
||||
// Explicitly set the file size in case there are dm-zero extents, which are
|
||||
// ignored below.
|
||||
if metadata.image_type == ImageType::Normal {
|
||||
for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() {
|
||||
output
|
||||
.set_len(block_device.size)
|
||||
.with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?;
|
||||
}
|
||||
}
|
||||
|
||||
slot.groups
|
||||
.par_iter()
|
||||
// Flatten grouped partitions.
|
||||
.flat_map(|group| &group.partitions)
|
||||
// Flatten extents in all partitions and split them to smaller chunks
|
||||
// for better parallelism.
|
||||
.flat_map(|partition| split_extents(&partition.extents))
|
||||
.map(|extent| {
|
||||
// Never fails for PSeekFiles.
|
||||
let mut reader = inputs[extent.device_index].reopen()?;
|
||||
let mut writer = outputs[extent.device_index].reopen()?;
|
||||
|
||||
let r_path = &cli.input[extent.device_index];
|
||||
let w_path = &cli.output[extent.device_index];
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(extent.lp_offset))
|
||||
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(extent.lp_offset))
|
||||
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(lp_cli: &LpCli, cli: &InfoCli) -> Result<()> {
|
||||
let (_, metadata) = open_lp_inputs(&[&cli.input])?;
|
||||
|
||||
// Unlike the other subcommands, we show all metadata slots here.
|
||||
display_metadata(lp_cli, &metadata);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lp_main(cli: &LpCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
LpCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
|
||||
LpCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
|
||||
LpCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
|
||||
LpCommand::Info(c) => info_subcommand(cli, c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack an LP image.
|
||||
///
|
||||
/// The LP image metadata is written to the info TOML file. For normal images,
|
||||
/// each partition is extracted to `<partition name>.img` in the output images
|
||||
/// directory. For empty images, the output images directory is unused.
|
||||
///
|
||||
/// If any partition names are unsafe to use in a path, the extraction process
|
||||
/// will fail and exit. Extracted files are never written outside of the tree
|
||||
/// directory, even if an external process tries to interfere.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input LP images.
|
||||
///
|
||||
/// If there are multiple images, they must be specified in order. If the
|
||||
/// order is unknown, run `avbroot lp info` against the `super` image and
|
||||
/// look at the `block_devices` field.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
|
||||
input: Vec<PathBuf>,
|
||||
|
||||
/// Path to output info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")]
|
||||
output_info: PathBuf,
|
||||
|
||||
/// Path to output images directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
|
||||
output_images: PathBuf,
|
||||
|
||||
/// The LP metadata slot to use.
|
||||
///
|
||||
/// This slot is the only slot where data extents are copied from. Any data
|
||||
/// referenced exclusively by other slots (if any) will be ignored.
|
||||
///
|
||||
/// This option is required if not all slots are identical.
|
||||
#[arg(short, long)]
|
||||
slot: Option<u32>,
|
||||
}
|
||||
|
||||
/// Pack an LP image.
|
||||
///
|
||||
/// For normal images, the number of metadata slots written is equal to the
|
||||
/// `metadata_slot_count` value in the info TOML. Each slot has identical
|
||||
/// metadata. It is not possible to write multiple slots with different metadata
|
||||
/// using this tool. For empty images, only a single slot is written, regardless
|
||||
/// of the value of `metadata_slot_count`, as required by the file format.
|
||||
///
|
||||
/// The new LP image will *only* contain images listed in the info TOML file and
|
||||
/// they are added in the order listed. The input images directory is not used
|
||||
/// when packing an empty image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output LP images.
|
||||
///
|
||||
/// If there are multiple images, they must be specified in the same order
|
||||
/// as the block device entries are listed in the info TOML.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
|
||||
output: Vec<PathBuf>,
|
||||
|
||||
/// Path to input info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to input images directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
|
||||
input_images: PathBuf,
|
||||
}
|
||||
|
||||
/// Repack an LP image.
|
||||
///
|
||||
/// This command is equivalent to running `unpack` and `pack`, except without
|
||||
/// storing the unpacked data to disk.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input LP images.
|
||||
///
|
||||
/// If there are multiple images, they must be specified in order. If the
|
||||
/// order is unknown, run `avbroot lp info` against the `super` image and
|
||||
/// look at the `block_devices` field.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
|
||||
input: Vec<PathBuf>,
|
||||
|
||||
/// Path to output LP images.
|
||||
///
|
||||
/// The number of output images must equal the number of input images.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
|
||||
output: Vec<PathBuf>,
|
||||
|
||||
/// The LP metadata slot to use.
|
||||
///
|
||||
/// This slot is the only slot where data extents are copied to the output
|
||||
/// images. Any data referenced exclusively by other slots (if any) will be
|
||||
/// ignored.
|
||||
///
|
||||
/// This option is required if not all slots are identical.
|
||||
#[arg(short, long)]
|
||||
slot: Option<u32>,
|
||||
}
|
||||
|
||||
/// Display LP image metadata.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input LP image.
|
||||
///
|
||||
/// If there are multiple images, this should refer to the first one, which
|
||||
/// is usually the `super` image. The other images are not needed when
|
||||
/// inspecting the metadata because the metadata is only stored in the first
|
||||
/// image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum LpCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect LP images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct LpCli {
|
||||
#[command(subcommand)]
|
||||
command: LpCommand,
|
||||
|
||||
/// Don't print LP metadata information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod args;
|
||||
pub mod avb;
|
||||
@@ -11,4 +9,7 @@ pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod key;
|
||||
pub mod lp;
|
||||
pub mod ota;
|
||||
pub mod payload;
|
||||
pub mod sparse;
|
||||
|
||||
+917
-293
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,471 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
cli::ota,
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::payload::{PayloadHeader, PayloadWriter},
|
||||
stream::{self, FromReader, PSeekFile},
|
||||
};
|
||||
|
||||
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
|
||||
let mut reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open payload for reading: {path:?}"))?;
|
||||
let header = PayloadHeader::from_reader(&mut reader)
|
||||
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
|
||||
if !allow_delta && !header.is_full_ota() {
|
||||
bail!("Payload is a delta OTA, not a full OTA");
|
||||
}
|
||||
|
||||
Ok((reader, header))
|
||||
}
|
||||
|
||||
fn open_writer(
|
||||
path: &Path,
|
||||
header: PayloadHeader,
|
||||
key: RsaSigningKey,
|
||||
) -> Result<PayloadWriter<BufWriter<File>>> {
|
||||
let writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open payload for writing: {path:?}"))?;
|
||||
let payload_writer = PayloadWriter::new(writer, header, key)
|
||||
.with_context(|| format!("Failed to write payload header: {path:?}"))?;
|
||||
|
||||
Ok(payload_writer)
|
||||
}
|
||||
|
||||
fn read_info(path: &Path) -> Result<PayloadHeader> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read payload info TOML: {path:?}"))?;
|
||||
let info = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse payload info TOML: {path:?}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn write_info(path: &Path, manifest: &PayloadHeader) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(manifest)
|
||||
.with_context(|| format!("Failed to serialize payload info TOML: {path:?}"))?;
|
||||
fs::write(path, data)
|
||||
.with_context(|| format!("Failed to write payload info TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_header(cli: &PayloadCli, header: &PayloadHeader) {
|
||||
if !cli.quiet {
|
||||
println!("{header:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_key(group: &KeyGroup) -> Result<RsaSigningKey> {
|
||||
let source = PassphraseSource::new(
|
||||
&group.key,
|
||||
group.pass_file.as_deref(),
|
||||
group.pass_env_var.as_deref(),
|
||||
);
|
||||
let signing_key = if let Some(helper) = &group.signing_helper {
|
||||
let public_key = crypto::read_pem_public_key_file(&group.key)
|
||||
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
|
||||
|
||||
RsaSigningKey::External {
|
||||
program: helper.clone(),
|
||||
public_key_file: group.key.clone(),
|
||||
public_key,
|
||||
passphrase_source: source,
|
||||
}
|
||||
} else {
|
||||
let private_key = crypto::read_pem_key_file(&group.key, &source)
|
||||
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
|
||||
|
||||
RsaSigningKey::Internal(private_key)
|
||||
};
|
||||
|
||||
Ok(signing_key)
|
||||
}
|
||||
|
||||
fn unpack_subcommand(
|
||||
payload_cli: &PayloadCli,
|
||||
cli: &UnpackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut reader, header) = open_reader(&cli.input, false)?;
|
||||
let payload_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
|
||||
|
||||
display_header(payload_cli, &header);
|
||||
|
||||
write_info(&cli.output_info, &header)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
|
||||
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
|
||||
|
||||
ota::extract_payload(
|
||||
&PSeekFile::new(reader.into_inner()),
|
||||
&directory,
|
||||
0,
|
||||
payload_size,
|
||||
&header,
|
||||
&header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(
|
||||
payload_cli: &PayloadCli,
|
||||
cli: &PackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let signing_key = load_key(&cli.key)?;
|
||||
|
||||
let mut header = read_info(&cli.input_info)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
|
||||
|
||||
for p in &header.manifest.partitions {
|
||||
let name = &p.partition_name;
|
||||
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-open all of the image files.
|
||||
let input_files = header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let path = format!("{}.img", p.partition_name);
|
||||
let file = directory
|
||||
.open(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))?;
|
||||
|
||||
Ok((p.partition_name.clone(), file))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
|
||||
// Compress the images and compute the list of install operations for
|
||||
// insertion into the payload header. The compressed data is stored in new
|
||||
// temp files and the original input files are dropped.
|
||||
let mut compressed_files = input_files
|
||||
.into_iter()
|
||||
.map(|(name, mut input_file)| {
|
||||
ota::compress_image(&name, &mut input_file, &mut header, None, cancel_signal)
|
||||
.with_context(|| format!("Failed to compress image: {name}"))?;
|
||||
|
||||
Ok((name, input_file))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
|
||||
info!("Generating new OTA payload");
|
||||
|
||||
// Now we can write the actual payload. With everything precomputed, this is
|
||||
// mostly just a simple copy.
|
||||
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
|
||||
|
||||
while payload_writer
|
||||
.begin_next_operation()
|
||||
.context("Failed to begin next payload blob entry")?
|
||||
{
|
||||
let name = payload_writer.partition().unwrap().partition_name.clone();
|
||||
let operation = payload_writer.operation().unwrap();
|
||||
|
||||
let Some(data_length) = operation.data_length else {
|
||||
// Otherwise, this is a ZERO/DISCARD operation.
|
||||
continue;
|
||||
};
|
||||
|
||||
let pi = payload_writer.partition_index().unwrap();
|
||||
let oi = payload_writer.operation_index().unwrap();
|
||||
let orig_partition = &header.manifest.partitions[pi];
|
||||
let orig_operation = &orig_partition.operations[oi];
|
||||
let data_offset = orig_operation
|
||||
.data_offset
|
||||
.ok_or_else(|| anyhow!("Missing data_offset in partition #{pi} operation #{oi}"))?;
|
||||
|
||||
// The compressed chunks are laid out sequentially and data_offset is
|
||||
// set to the offset within that file.
|
||||
let Some(input_file) = compressed_files.get_mut(&name) else {
|
||||
unreachable!("Compressed data not found for image: {name}");
|
||||
};
|
||||
|
||||
input_file
|
||||
.seek(SeekFrom::Start(data_offset))
|
||||
.with_context(|| format!("Failed to seek image: {name}"))?;
|
||||
|
||||
stream::copy_n(input_file, &mut payload_writer, data_length, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
|
||||
}
|
||||
|
||||
let (_, header, properties, _) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
// Display the header information now that it has been finalized.
|
||||
display_header(payload_cli, &header);
|
||||
|
||||
// Optionally, write payload_properties.txt.
|
||||
if let Some(path) = &cli.output_properties {
|
||||
fs::write(path, properties)
|
||||
.with_context(|| format!("Failed to write payload properties: {path:?}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(
|
||||
payload_cli: &PayloadCli,
|
||||
cli: &RepackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let signing_key = load_key(&cli.key)?;
|
||||
|
||||
let (mut reader, header) = open_reader(&cli.input, true)?;
|
||||
|
||||
info!("Generating new OTA payload");
|
||||
|
||||
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
|
||||
|
||||
while payload_writer
|
||||
.begin_next_operation()
|
||||
.context("Failed to begin next payload blob entry")?
|
||||
{
|
||||
let name = payload_writer.partition().unwrap().partition_name.clone();
|
||||
let operation = payload_writer.operation().unwrap();
|
||||
|
||||
let Some(data_length) = operation.data_length else {
|
||||
// Otherwise, this is a ZERO/DISCARD operation.
|
||||
continue;
|
||||
};
|
||||
|
||||
let pi = payload_writer.partition_index().unwrap();
|
||||
let oi = payload_writer.operation_index().unwrap();
|
||||
let orig_partition = &header.manifest.partitions[pi];
|
||||
let orig_operation = &orig_partition.operations[oi];
|
||||
let data_offset = orig_operation
|
||||
.data_offset
|
||||
.ok_or_else(|| anyhow!("Missing data_offset in partition #{pi} operation #{oi}"))?;
|
||||
|
||||
// Directly copy blobs from the original payload.
|
||||
let data_offset = data_offset
|
||||
.checked_add(header.blob_offset)
|
||||
.ok_or_else(|| anyhow!("data_offset overflow in partition #{pi} operation #{oi}"))?;
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(data_offset))
|
||||
.with_context(|| format!("Failed to seek original payload to {data_offset}"))?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut payload_writer, data_length, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
|
||||
}
|
||||
|
||||
let (_, header, properties, _) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
// Display the header information now that it has been finalized.
|
||||
display_header(payload_cli, &header);
|
||||
|
||||
// Optionally, write payload_properties.txt.
|
||||
if let Some(path) = &cli.output_properties {
|
||||
fs::write(path, properties)
|
||||
.with_context(|| format!("Failed to write payload properties: {path:?}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(payload_cli: &PayloadCli, cli: &InfoCli) -> Result<()> {
|
||||
let (_, header) = open_reader(&cli.input, true)?;
|
||||
|
||||
display_header(payload_cli, &header);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn payload_main(cli: &PayloadCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
PayloadCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
|
||||
PayloadCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
|
||||
PayloadCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
|
||||
PayloadCommand::Info(c) => info_subcommand(cli, c),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct KeyGroup {
|
||||
/// Path to signing key.
|
||||
///
|
||||
/// This should normally be a private key. However, if --signing-helper is
|
||||
/// used, then it should be a public key instead.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: PathBuf,
|
||||
|
||||
/// Environment variable containing private key passphrase.
|
||||
#[arg(long, value_name = "ENV_VAR", value_parser, group = "pass")]
|
||||
pass_env_var: Option<OsString>,
|
||||
|
||||
/// File containing private key passphrase.
|
||||
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
|
||||
pass_file: Option<PathBuf>,
|
||||
|
||||
/// External program for signing.
|
||||
///
|
||||
/// If this option is specified, then --key must refer to a public key. The
|
||||
/// program will be invoked as:
|
||||
///
|
||||
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
|
||||
#[arg(long, value_name = "PROGRAM", value_parser)]
|
||||
signing_helper: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Unpack a payload binary.
|
||||
///
|
||||
/// Each partition is extracted to `<partition name>.img` in the output images
|
||||
/// directory. The payload header metadata is written to the info TOML file.
|
||||
///
|
||||
/// If any partition names are unsafe to use in a path, the extraction process
|
||||
/// will fail and exit. Extracted files are never written outside of the tree
|
||||
/// directory, even if an external process tries to interfere.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input payload binary.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output info TOML.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "payload.toml"
|
||||
)]
|
||||
output_info: PathBuf,
|
||||
|
||||
/// Path to output images directory.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIR",
|
||||
value_parser,
|
||||
default_value = "payload_images"
|
||||
)]
|
||||
output_images: PathBuf,
|
||||
}
|
||||
|
||||
/// Pack a payload binary.
|
||||
///
|
||||
/// The new payload binary will *only* contain images listed in the info TOML
|
||||
/// file. Extra images in the input images directory that aren't listed will be
|
||||
/// silently ignored. Images are added to the payload in the order that they are
|
||||
/// listed in the info TOML file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output payload binary.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to output payload properties file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output_properties: Option<PathBuf>,
|
||||
|
||||
/// Path to input info TOML.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "payload.toml"
|
||||
)]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to input images directory.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIR",
|
||||
value_parser,
|
||||
default_value = "payload_images"
|
||||
)]
|
||||
input_images: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
}
|
||||
|
||||
/// Repack a payload binary.
|
||||
///
|
||||
/// This command is equivalent to running `unpack` and `pack`, except without
|
||||
/// storing the unpacked data to disk nor recompressing the partition images.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input payload binary.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output payload binary.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to output payload properties file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output_properties: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
}
|
||||
|
||||
/// Display payload information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input payload file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum PayloadCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect OTA payloads.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PayloadCli {
|
||||
#[command(subcommand)]
|
||||
command: PayloadCommand,
|
||||
|
||||
/// Don't print payload header information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use crc32fast::Hasher;
|
||||
use zerocopy::{IntoBytes, little_endian};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
padding,
|
||||
sparse::{
|
||||
self, Chunk, ChunkBounds, ChunkData, ChunkList, CrcMode, Header, SparseReader,
|
||||
SparseWriter,
|
||||
},
|
||||
},
|
||||
stream,
|
||||
};
|
||||
|
||||
struct CompactView<'a, T>(&'a [T]);
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for CompactView<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
|
||||
for item in self.0 {
|
||||
// No alternate mode for no inner newlines.
|
||||
list.entry(&format_args!("{item:?}"));
|
||||
}
|
||||
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Metadata {
|
||||
header: Header,
|
||||
chunks: Vec<Chunk>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Metadata {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Metadata")
|
||||
.field("header", &self.header)
|
||||
.field("chunks", &CompactView(&self.chunks))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn open_reader(path: &Path) -> Result<File> {
|
||||
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))
|
||||
}
|
||||
|
||||
fn open_writer(path: &Path, truncate: bool) -> Result<File> {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(truncate)
|
||||
.open(path)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))
|
||||
}
|
||||
|
||||
fn display_metadata(cli: &SparseCli, metadata: &Metadata) {
|
||||
if !cli.quiet {
|
||||
println!("{metadata:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits large data chunks to ensure that none exceed 64 MiB. This is not
|
||||
/// necessary in most cases, but is kept to match the behavior of AOSP's
|
||||
/// libsparse.
|
||||
fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
|
||||
const MAX_BYTES: u32 = 64 * 1024 * 1024;
|
||||
|
||||
let max_blocks_per_chunk = MAX_BYTES / block_size;
|
||||
let mut result = vec![];
|
||||
|
||||
for mut chunk in chunks.iter().copied() {
|
||||
if chunk.data == ChunkData::Data {
|
||||
while chunk.bounds.len() > max_blocks_per_chunk {
|
||||
result.push(Chunk {
|
||||
bounds: ChunkBounds {
|
||||
start: chunk.bounds.start,
|
||||
end: chunk.bounds.start + max_blocks_per_chunk,
|
||||
},
|
||||
data: chunk.data,
|
||||
});
|
||||
|
||||
chunk.bounds.start += max_blocks_per_chunk;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(chunk);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// [Linux only] Find allocated regions of the file. This avoids needing to read
|
||||
/// unused portions of the file if it is a native sparse file.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn find_allocated_regions(
|
||||
path: &Path,
|
||||
reader: &File,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
use rustix::{fs::SeekFrom, io::Errno};
|
||||
|
||||
let mut result = vec![];
|
||||
let mut start;
|
||||
let mut end = 0;
|
||||
|
||||
loop {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
start = match rustix::fs::seek(reader, SeekFrom::Data(end)) {
|
||||
Ok(offset) => offset,
|
||||
Err(e) if e == Errno::NXIO => break,
|
||||
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
|
||||
};
|
||||
|
||||
end = rustix::fs::seek(reader, SeekFrom::Hole(start))
|
||||
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
|
||||
|
||||
result.push(start..end);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Compute chunk boundaries for the list of potentially overlapping file byte
|
||||
/// regions. If `exact_bounds` is true, then the regions must be block-aligned.
|
||||
/// Otherwise, the lower boundaries are aligned down and the upper boundaries
|
||||
/// are aligned up.
|
||||
fn get_chunks_for_regions(
|
||||
block_size: u32,
|
||||
file_size: u64,
|
||||
file_regions: &[Range<u64>],
|
||||
exact_bounds: bool,
|
||||
) -> Result<(u32, Vec<ChunkBounds>)> {
|
||||
let block_size_64 = u64::from(block_size);
|
||||
|
||||
let file_blocks: u32 = (file_size / u64::from(block_size))
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("File size {file_size} too large for block size {block_size}"))?;
|
||||
|
||||
let mut chunk_list = ChunkList::new();
|
||||
chunk_list.set_len(file_blocks);
|
||||
|
||||
for region in file_regions {
|
||||
let mut start_byte = region.start;
|
||||
let mut end_byte = region.end;
|
||||
|
||||
if exact_bounds {
|
||||
if start_byte % block_size_64 != 0 || end_byte % block_size_64 != 0 {
|
||||
bail!("File region bounds are not block-aligned: {region:?}");
|
||||
}
|
||||
} else {
|
||||
start_byte = start_byte / block_size_64 * block_size_64;
|
||||
end_byte = padding::round(end_byte, block_size_64).unwrap();
|
||||
}
|
||||
|
||||
let start_block: u32 = (start_byte / block_size_64).try_into().map_err(|_| {
|
||||
anyhow!("Region start offset {start_byte} too large for block size {block_size}")
|
||||
})?;
|
||||
let end_block: u32 = (end_byte / block_size_64).try_into().map_err(|_| {
|
||||
anyhow!("Region end offset {end_byte} too large for block size {block_size}")
|
||||
})?;
|
||||
|
||||
chunk_list.insert_data(ChunkBounds {
|
||||
start: start_block,
|
||||
end: end_block,
|
||||
});
|
||||
}
|
||||
|
||||
let chunks = chunk_list.iter_allocated().map(|c| c.bounds).collect();
|
||||
|
||||
Ok((file_blocks, chunks))
|
||||
}
|
||||
|
||||
/// Compute the sparse [`Chunk`]s needed to cover the specified regions.
|
||||
fn compute_chunks(
|
||||
path: &Path,
|
||||
reader: &mut File,
|
||||
block_size: u32,
|
||||
file_blocks: u32,
|
||||
block_regions: &[ChunkBounds],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(ChunkList, u32)> {
|
||||
let mut chunk_list = ChunkList::new();
|
||||
let mut hasher = Some(Hasher::new());
|
||||
let mut buf = vec![0u8; block_size as usize];
|
||||
let mut block = 0;
|
||||
|
||||
chunk_list.set_len(file_blocks);
|
||||
|
||||
for bounds in block_regions {
|
||||
if bounds.start != block {
|
||||
// Not contiguous so we cannot compute the checksum.
|
||||
hasher = None;
|
||||
}
|
||||
|
||||
let offset = u64::from(bounds.start) * u64::from(block_size);
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
for block in *bounds {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.with_context(|| format!("Failed to read full block: {path:?}"))?;
|
||||
|
||||
if let Some(h) = &mut hasher {
|
||||
h.update(&buf);
|
||||
}
|
||||
|
||||
let new_bounds = ChunkBounds {
|
||||
start: block,
|
||||
end: block + 1,
|
||||
};
|
||||
|
||||
if buf.chunks_exact(4).all(|c| c == &buf[..4]) {
|
||||
let fill_value = u32::from_le_bytes(buf[..4].try_into().unwrap());
|
||||
chunk_list.insert_fill(new_bounds, fill_value);
|
||||
} else {
|
||||
chunk_list.insert_data(new_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
block = bounds.end;
|
||||
}
|
||||
|
||||
if block != file_blocks {
|
||||
hasher = None;
|
||||
}
|
||||
|
||||
let crc32 = hasher.map(|h| h.finalize()).unwrap_or_default();
|
||||
|
||||
Ok((chunk_list, crc32))
|
||||
}
|
||||
|
||||
fn unpack_subcommand(
|
||||
sparse_cli: &SparseCli,
|
||||
cli: &UnpackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let reader = open_reader(&cli.input)?;
|
||||
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate)
|
||||
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
|
||||
|
||||
let mut metadata = Metadata {
|
||||
header: sparse_reader.header(),
|
||||
chunks: vec![],
|
||||
};
|
||||
|
||||
let mut writer = open_writer(&cli.output, !cli.preserve)?;
|
||||
|
||||
if cli.preserve {
|
||||
let expected_size =
|
||||
u64::from(metadata.header.num_blocks) * u64::from(metadata.header.block_size);
|
||||
let file_size = writer
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.output))?;
|
||||
|
||||
if file_size < expected_size {
|
||||
writer
|
||||
.set_len(expected_size)
|
||||
.with_context(|| format!("Failed to set file size: {:?}", cli.output))?;
|
||||
}
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Start(0))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
|
||||
}
|
||||
|
||||
while let Some(chunk) = sparse_reader
|
||||
.next_chunk()
|
||||
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
|
||||
{
|
||||
match chunk.data {
|
||||
ChunkData::Fill(value) => {
|
||||
let fill_value = little_endian::U32::from(value);
|
||||
let buf = vec![fill_value; metadata.header.block_size as usize / 4];
|
||||
|
||||
for _ in chunk.bounds {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
writer
|
||||
.write_all(buf.as_bytes())
|
||||
.with_context(|| format!("Failed to write data: {:?}", cli.output))?;
|
||||
}
|
||||
}
|
||||
ChunkData::Data => {
|
||||
// This cannot overflow.
|
||||
let to_copy = chunk.bounds.len() * metadata.header.block_size;
|
||||
|
||||
stream::copy_n(
|
||||
&mut sparse_reader,
|
||||
&mut writer,
|
||||
to_copy.into(),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output)
|
||||
})?;
|
||||
}
|
||||
ChunkData::Hole => {
|
||||
// This cannot overflow.
|
||||
let to_skip = chunk.bounds.len() * metadata.header.block_size;
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Current(to_skip.into()))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
|
||||
}
|
||||
ChunkData::Crc32(_) => {}
|
||||
}
|
||||
|
||||
metadata.chunks.push(chunk);
|
||||
}
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
sparse_reader
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(
|
||||
sparse_cli: &SparseCli,
|
||||
cli: &PackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
if cli.block_size == 0 || cli.block_size % 4 != 0 {
|
||||
bail!(
|
||||
"Block size must be a non-zero multiple of 4: {}",
|
||||
cli.block_size,
|
||||
);
|
||||
}
|
||||
|
||||
let mut reader = open_reader(&cli.input)?;
|
||||
|
||||
let file_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
|
||||
if file_size % u64::from(cli.block_size) != 0 {
|
||||
bail!(
|
||||
"File size {file_size} is not a multiple of block size {}",
|
||||
cli.block_size,
|
||||
);
|
||||
}
|
||||
|
||||
// Compute the byte regions to pack into the sparse file.
|
||||
let (file_regions, exact_bounds) = if !cli.region.is_empty() {
|
||||
let regions = cli
|
||||
.region
|
||||
.chunks_exact(2)
|
||||
.map(|c| c[0]..c[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(regions, false)
|
||||
} else {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
let regions = find_allocated_regions(&cli.input, &reader, cancel_signal)?;
|
||||
|
||||
(regions, false)
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
{
|
||||
#[allow(clippy::single_range_in_vec_init)]
|
||||
(vec![0..file_size], true)
|
||||
}
|
||||
};
|
||||
|
||||
// Get the file regions as non-overlapping and sorted block regions.
|
||||
let (file_blocks, block_regions) =
|
||||
get_chunks_for_regions(cli.block_size, file_size, &file_regions, exact_bounds)?;
|
||||
|
||||
// Compute the checksum (if possible) and the list of actual chunks.
|
||||
let (chunk_list, crc32) = compute_chunks(
|
||||
&cli.input,
|
||||
&mut reader,
|
||||
cli.block_size,
|
||||
file_blocks,
|
||||
&block_regions,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
let chunks = split_chunks(&chunk_list.to_chunks(), cli.block_size);
|
||||
let metadata = Metadata {
|
||||
header: Header {
|
||||
major_version: sparse::MAJOR_VERSION,
|
||||
minor_version: sparse::MINOR_VERSION,
|
||||
block_size: cli.block_size,
|
||||
num_blocks: chunk_list.len(),
|
||||
// This can't overflow because the number of chunks is always
|
||||
// smaller than the number of blocks (because we don't add CRC32
|
||||
// chunks).
|
||||
num_chunks: chunks.len() as u32,
|
||||
// This will be zero if the regions don't span the entire file.
|
||||
crc32,
|
||||
},
|
||||
chunks,
|
||||
};
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
let writer = open_writer(&cli.output, true)?;
|
||||
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
|
||||
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
|
||||
|
||||
for chunk in metadata.chunks {
|
||||
sparse_writer
|
||||
.start_chunk(chunk)
|
||||
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
|
||||
|
||||
if chunk.data == ChunkData::Data {
|
||||
let offset = u64::from(chunk.bounds.start) * u64::from(cli.block_size);
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.input))?;
|
||||
|
||||
let to_copy = u64::from(chunk.bounds.len()) * u64::from(cli.block_size);
|
||||
|
||||
stream::copy_n(&mut reader, &mut sparse_writer, to_copy, cancel_signal).with_context(
|
||||
|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
sparse_writer
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(
|
||||
sparse_cli: &SparseCli,
|
||||
cli: &RepackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let reader = open_reader(&cli.input)?;
|
||||
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Validate)
|
||||
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
|
||||
|
||||
let mut metadata = Metadata {
|
||||
header: sparse_reader.header(),
|
||||
chunks: vec![],
|
||||
};
|
||||
|
||||
let writer = open_writer(&cli.output, true)?;
|
||||
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
|
||||
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
|
||||
|
||||
while let Some(chunk) = sparse_reader
|
||||
.next_chunk()
|
||||
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
|
||||
{
|
||||
sparse_writer
|
||||
.start_chunk(chunk)
|
||||
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
|
||||
|
||||
if chunk.data == ChunkData::Data {
|
||||
// This cannot overflow.
|
||||
let to_copy = chunk.bounds.len() * metadata.header.block_size;
|
||||
|
||||
stream::copy_n(
|
||||
&mut sparse_reader,
|
||||
&mut sparse_writer,
|
||||
to_copy.into(),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output))?;
|
||||
}
|
||||
|
||||
metadata.chunks.push(chunk);
|
||||
}
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
sparse_reader
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
|
||||
sparse_writer
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(sparse_cli: &SparseCli, cli: &InfoCli) -> Result<()> {
|
||||
let reader = open_reader(&cli.input)?;
|
||||
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Ignore)
|
||||
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
|
||||
|
||||
let mut metadata = Metadata {
|
||||
header: sparse_reader.header(),
|
||||
chunks: vec![],
|
||||
};
|
||||
|
||||
while let Some(chunk) = sparse_reader
|
||||
.next_chunk()
|
||||
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
|
||||
{
|
||||
metadata.chunks.push(chunk);
|
||||
}
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sparse_main(cli: &SparseCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
SparseCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
|
||||
SparseCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
|
||||
SparseCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
|
||||
SparseCommand::Info(c) => info_subcommand(cli, c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack a sparse image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output raw image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Preserve existing data in the output file.
|
||||
///
|
||||
/// This is useful when unpacking multiple sparse files into a single output
|
||||
/// file because they contain disjoint blocks of data.
|
||||
#[arg(long)]
|
||||
preserve: bool,
|
||||
}
|
||||
|
||||
/// Pack a sparse image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output sparse image.
|
||||
///
|
||||
/// If `--region` is not used and the input file is not a (native) sparse
|
||||
/// file on Linux, then the output sparse image is written with a CRC32
|
||||
/// checksum in the header.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input raw image.
|
||||
///
|
||||
/// On Linux, if this is a (native) sparse file, then the unallocated
|
||||
/// sections of the file will be skipped and will be stored in the output
|
||||
/// file as hole chunks.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Block size.
|
||||
#[arg(short, long, value_name = "BYTES", default_value_t = 4096)]
|
||||
block_size: u32,
|
||||
|
||||
/// Pack certain byte regions from the file.
|
||||
///
|
||||
/// The start offset will be aligned down to the block size and the end
|
||||
/// offset will be aligned up. This option can be specified any number of
|
||||
/// times and in any order. Overlapping regions are allowed.
|
||||
///
|
||||
/// Unused regions will be stored in the sparse file as hole chunks.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
region: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Repack a sparse image.
|
||||
///
|
||||
/// This command is equivalent to running `unpack` and `pack`, except without
|
||||
/// storing the unpacked data to disk.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
/// Display sparse image metadata.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum SparseCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect sparse images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SparseCli {
|
||||
#[command(subcommand)]
|
||||
command: SparseCommand,
|
||||
|
||||
/// Don't print sparse image metadata.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
+418
-95
@@ -1,14 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
env::{self, VarError},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -20,54 +19,123 @@ use cms::{
|
||||
SignedData, SignerIdentifier, SignerInfo, SignerInfos,
|
||||
},
|
||||
};
|
||||
use passterm::PromptError;
|
||||
use pkcs8::{
|
||||
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
|
||||
LineEnding, PrivateKeyInfo,
|
||||
pkcs5::{pbes2, scrypt},
|
||||
DecodePrivateKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo, LineEnding,
|
||||
PrivateKeyInfo,
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rsa::{pkcs1v15::SigningKey, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use sha2::Sha256;
|
||||
use rsa::{
|
||||
Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, pkcs1v15::SigningKey, traits::PublicKeyParts,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{
|
||||
Certificate,
|
||||
builder::{Builder, CertificateBuilder, Profile},
|
||||
der::{pem::PemLabel, referenced::OwnedToRef, Any, Decode, DecodePem, EncodePem},
|
||||
der::{Any, Decode, DecodePem, EncodePem, pem::PemLabel, referenced::OwnedToRef},
|
||||
serial_number::SerialNumber,
|
||||
spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned},
|
||||
time::Validity,
|
||||
Certificate,
|
||||
};
|
||||
|
||||
use crate::util::DebugString;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Signature algorithm not supported: {0:?}")]
|
||||
UnsupportedAlgorithm(SignatureAlgorithm),
|
||||
#[error("RSA key size ({}) not supported", .0 * 8)]
|
||||
UnsupportedKeySize(usize),
|
||||
#[error("Invalid digest length ({0} bytes) for {1:?}")]
|
||||
InvalidDigestLength(usize, SignatureAlgorithm),
|
||||
#[error("Invalid signature length ({0} bytes) for {1:?}")]
|
||||
InvalidSignatureLength(usize, SignatureAlgorithm),
|
||||
#[error("Failed to run command: {0:?}")]
|
||||
CommandSpawn(DebugString, #[source] io::Error),
|
||||
#[error("Command failed with status: {1}: {0:?}")]
|
||||
CommandExecution(DebugString, ExitStatus),
|
||||
#[error("Signature from signing helper does not match public key: {0:?}")]
|
||||
SigningHelperBadSignature(PathBuf),
|
||||
#[error("Passphrase prompt requires an interactive terminal")]
|
||||
NotInteractive(#[source] io::Error),
|
||||
#[error("Failed to prompt for passphrase")]
|
||||
PassphrasePrompt(#[source] PromptError),
|
||||
#[error("Passphrases do not match")]
|
||||
ConfirmPassphrase,
|
||||
#[error("Failed to read environment variable: {0:?}")]
|
||||
InvalidEnvVar(OsString, #[source] VarError),
|
||||
#[error("PEM has start tag, but no end tag")]
|
||||
PemNoEndTag,
|
||||
#[error("Failed to load encrypted private key")]
|
||||
#[error("Failed to load encrypted RSA private key")]
|
||||
LoadKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to load unencrypted private key")]
|
||||
#[error("Failed to load unencrypted RSA private key")]
|
||||
LoadKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save encrypted private key")]
|
||||
#[error("Failed to save encrypted RSA private key")]
|
||||
SaveKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save unencrypted private key")]
|
||||
#[error("Failed to save unencrypted RSA private key")]
|
||||
SaveKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("X509 error")]
|
||||
X509(#[from] x509_cert::builder::Error),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Failed to load RSA public key")]
|
||||
LoadPubKey(#[source] pkcs8::spki::Error),
|
||||
#[error("Failed to save RSA public key")]
|
||||
SavePubKey(#[source] pkcs8::spki::Error),
|
||||
#[error("Failed to load X509 certificate")]
|
||||
LoadCert(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to save X509 certificate")]
|
||||
SaveCert(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to generate RSA key")]
|
||||
RsaGenerate(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to RSA sign digest")]
|
||||
RsaSign(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to generate X509 certificate")]
|
||||
CertGenerate(#[source] x509_cert::builder::Error),
|
||||
#[error("Invalid parameters for X509 certificate generation")]
|
||||
CertParams(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to CMS sign digest")]
|
||||
CmsSign(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to parse CMS signature")]
|
||||
CmsParse(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to read file: {0:?}")]
|
||||
ReadFile(PathBuf, #[source] io::Error),
|
||||
#[error("Failed to write file: {0:?}")]
|
||||
WriteFile(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub enum SignatureAlgorithm {
|
||||
Sha1WithRsa,
|
||||
Sha256WithRsa,
|
||||
Sha512WithRsa,
|
||||
}
|
||||
|
||||
impl SignatureAlgorithm {
|
||||
/// Length of digest required by the signing algorithm.
|
||||
pub fn digest_len(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1WithRsa => Sha1::output_size(),
|
||||
Self::Sha256WithRsa => Sha256::output_size(),
|
||||
Self::Sha512WithRsa => Sha512::output_size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the digest of the specified data.
|
||||
pub fn hash(self, data: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Sha1WithRsa => Sha1::digest(data).to_vec(),
|
||||
Self::Sha256WithRsa => Sha256::digest(data).to_vec(),
|
||||
Self::Sha512WithRsa => Sha512::digest(data).to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PassphraseSource {
|
||||
Prompt(String),
|
||||
EnvVar(OsString),
|
||||
@@ -76,6 +144,7 @@ pub enum PassphraseSource {
|
||||
|
||||
impl PassphraseSource {
|
||||
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
if let Some(v) = env_var {
|
||||
Self::EnvVar(v.to_owned())
|
||||
} else if let Some(p) = pass_file {
|
||||
@@ -85,13 +154,33 @@ impl PassphraseSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(prompt: &str) -> Result<String> {
|
||||
match passterm::prompt_password_tty(Some(prompt)) {
|
||||
Ok(p) => Ok(p),
|
||||
Err(e) => {
|
||||
#[cfg(unix)]
|
||||
if let PromptError::IOError(io_e) = e {
|
||||
if let Some(errno) = io_e.raw_os_error() {
|
||||
if errno == libc::ENXIO || errno == libc::ENOTTY {
|
||||
return Err(Error::NotInteractive(io_e));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(Error::PassphrasePrompt(PromptError::IOError(io_e)));
|
||||
}
|
||||
|
||||
Err(Error::PassphrasePrompt(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquire(&self, confirm: bool) -> Result<String> {
|
||||
let passphrase = match self {
|
||||
Self::Prompt(p) => {
|
||||
let first = rpassword::prompt_password(p)?;
|
||||
let first = Self::prompt(p)?;
|
||||
|
||||
if confirm {
|
||||
let second = rpassword::prompt_password("Confirm: ")?;
|
||||
let second = Self::prompt("Confirm: ")?;
|
||||
|
||||
if first != second {
|
||||
return Err(Error::ConfirmPassphrase);
|
||||
@@ -101,8 +190,9 @@ impl PassphraseSource {
|
||||
first
|
||||
}
|
||||
Self::EnvVar(v) => env::var(v).map_err(|e| Error::InvalidEnvVar(v.clone(), e))?,
|
||||
Self::File(p) => fs::read_to_string(p)?
|
||||
.trim_end_matches(&['\r', '\n'])
|
||||
Self::File(p) => fs::read_to_string(p)
|
||||
.map_err(|e| Error::ReadFile(p.clone(), e))?
|
||||
.trim_end_matches(['\r', '\n'])
|
||||
.to_owned(),
|
||||
};
|
||||
|
||||
@@ -110,12 +200,197 @@ impl PassphraseSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_key_size(size: usize) -> Result<()> {
|
||||
// RustCrypto does not support 8192-bit keys.
|
||||
if size > 4096 / 8 {
|
||||
return Err(Error::UnsupportedKeySize(size));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copied from rsa-0.9.6 since the function is not exported.
|
||||
fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> rsa::Result<Vec<u8>> {
|
||||
let hash_len = hashed.len();
|
||||
let t_len = prefix.len() + hashed.len();
|
||||
if k < t_len + 11 {
|
||||
return Err(rsa::Error::MessageTooLong);
|
||||
}
|
||||
|
||||
// EM = 0x00 || 0x01 || PS || 0x00 || T
|
||||
let mut em = vec![0xff; k];
|
||||
em[0] = 0;
|
||||
em[1] = 1;
|
||||
em[k - t_len - 1] = 0;
|
||||
em[k - t_len..k - hash_len].copy_from_slice(prefix);
|
||||
em[k - hash_len..k].copy_from_slice(hashed);
|
||||
|
||||
Ok(em)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RsaSigningKey {
|
||||
Internal(RsaPrivateKey),
|
||||
External {
|
||||
program: PathBuf,
|
||||
public_key_file: PathBuf,
|
||||
public_key: RsaPublicKey,
|
||||
passphrase_source: PassphraseSource,
|
||||
},
|
||||
}
|
||||
|
||||
impl RsaSigningKey {
|
||||
/// Size of key in bytes.
|
||||
pub fn size(&self) -> usize {
|
||||
match self {
|
||||
Self::Internal(key) => key.size(),
|
||||
Self::External { public_key, .. } => public_key.size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the public key portion of the signing key.
|
||||
pub fn to_public_key(&self) -> RsaPublicKey {
|
||||
match self {
|
||||
Self::Internal(key) => key.to_public_key(),
|
||||
Self::External { public_key, .. } => public_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign the digest with the specified signature algorithm.
|
||||
pub fn sign(&self, algo: SignatureAlgorithm, digest: &[u8]) -> Result<Vec<u8>> {
|
||||
if digest.len() != algo.digest_len() {
|
||||
return Err(Error::InvalidDigestLength(digest.len(), algo));
|
||||
}
|
||||
|
||||
check_key_size(self.size())?;
|
||||
|
||||
let scheme = match algo {
|
||||
// We don't support signing with insecure algorithms.
|
||||
SignatureAlgorithm::Sha1WithRsa => return Err(Error::UnsupportedAlgorithm(algo)),
|
||||
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
|
||||
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Internal(key) => key
|
||||
.sign(scheme, digest)
|
||||
.map_err(|e| Error::RsaSign(Box::new(e))),
|
||||
Self::External {
|
||||
program,
|
||||
public_key,
|
||||
public_key_file,
|
||||
passphrase_source,
|
||||
} => {
|
||||
let key_bits = public_key.size() * 8;
|
||||
let algo_str = match algo {
|
||||
SignatureAlgorithm::Sha1WithRsa => unreachable!(),
|
||||
SignatureAlgorithm::Sha256WithRsa => format!("SHA256_RSA{key_bits}"),
|
||||
SignatureAlgorithm::Sha512WithRsa => format!("SHA512_RSA{key_bits}"),
|
||||
};
|
||||
|
||||
let mut command = Command::new(program);
|
||||
command.arg(algo_str);
|
||||
command.arg(public_key_file);
|
||||
|
||||
match passphrase_source {
|
||||
PassphraseSource::Prompt(_) => {}
|
||||
PassphraseSource::EnvVar(v) => {
|
||||
command.arg("env");
|
||||
command.arg(v);
|
||||
}
|
||||
PassphraseSource::File(p) => {
|
||||
command.arg("file");
|
||||
command.arg(p);
|
||||
}
|
||||
}
|
||||
|
||||
command.stdin(Stdio::piped());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::inherit());
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
|
||||
|
||||
// We don't bother with spawning a thread. The pipe capacity on
|
||||
// all major OSs is significantly larger than the digest, so we
|
||||
// don't risk deadlocking even if the process doesn't read from
|
||||
// stdin.
|
||||
//
|
||||
// Pipe capacities:
|
||||
// * Linux: 64 KiB
|
||||
// * macOS: 4 KiB, 16 KiB (usually), or 64 KiB
|
||||
// * Windows: 4 KiB
|
||||
|
||||
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())
|
||||
.map_err(|e| Error::RsaSign(Box::new(e)))?;
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&padded_digest)
|
||||
.map_err(|e| Error::WriteFile("<signing helper stdin>".into(), e))?;
|
||||
|
||||
let child = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(Error::CommandExecution(
|
||||
DebugString::new(&command),
|
||||
child.status,
|
||||
));
|
||||
} else if child.stdout.len() != self.size() {
|
||||
return Err(Error::InvalidSignatureLength(child.stdout.len(), algo));
|
||||
}
|
||||
|
||||
// Check that the helper signed with the proper key.
|
||||
if let Err(e) = self.to_public_key().verify_sig(algo, digest, &child.stdout) {
|
||||
return match e {
|
||||
Error::RsaVerify(_) => {
|
||||
Err(Error::SigningHelperBadSignature(public_key_file.clone()))
|
||||
}
|
||||
e => Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(child.stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RsaPublicKeyExt {
|
||||
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()>;
|
||||
}
|
||||
|
||||
impl RsaPublicKeyExt for RsaPublicKey {
|
||||
/// Verify the signature against the specified key.
|
||||
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()> {
|
||||
// Check this explicitly so we can provide a better error message.
|
||||
if digest.len() != algo.digest_len() {
|
||||
return Err(Error::InvalidDigestLength(digest.len(), algo));
|
||||
}
|
||||
|
||||
check_key_size(self.size())?;
|
||||
|
||||
let scheme = match algo {
|
||||
SignatureAlgorithm::Sha1WithRsa => Pkcs1v15Sign::new::<Sha1>(),
|
||||
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
|
||||
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
|
||||
};
|
||||
|
||||
self.verify(scheme, digest, signature)
|
||||
.map_err(|e| Error::RsaVerify(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an 4096-bit RSA key pair.
|
||||
pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// avbroot supports 4096-bit keys only.
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096)?;
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096).map_err(|e| Error::RsaGenerate(Box::new(e)))?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
@@ -127,20 +402,27 @@ pub fn generate_cert(
|
||||
validity: Duration,
|
||||
subject: &str,
|
||||
) -> Result<Certificate> {
|
||||
let public_key_der = key.to_public_key().to_public_key_der()?;
|
||||
let public_key_der = key
|
||||
.to_public_key()
|
||||
.to_public_key_der()
|
||||
.map_err(Error::SavePubKey)?;
|
||||
let signing_key = SigningKey::<Sha256>::new(key.clone());
|
||||
|
||||
let builder = CertificateBuilder::new(
|
||||
Profile::Root,
|
||||
SerialNumber::from(serial),
|
||||
Validity::from_now(validity)?,
|
||||
subject.parse()?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())?,
|
||||
Validity::from_now(validity).map_err(Error::CertParams)?,
|
||||
subject.parse().map_err(Error::CertParams)?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())
|
||||
.map_err(Error::CertParams)?,
|
||||
&signing_key,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::CertGenerate)?;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let cert = builder.build_with_rng(&mut rng)?;
|
||||
let cert = builder
|
||||
.build_with_rng(&mut rng)
|
||||
.map_err(Error::CertGenerate)?;
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
@@ -164,6 +446,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 +458,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 {
|
||||
@@ -190,62 +475,92 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a reader.
|
||||
pub fn read_pem_cert(mut reader: impl Read) -> Result<Certificate> {
|
||||
pub fn read_pem_cert(path: &Path, mut reader: impl Read) -> Result<Certificate> {
|
||||
let mut data = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
reader
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
let data = reformat_pem(&data)?;
|
||||
let certificate = Certificate::from_pem(data)?;
|
||||
let certificate = Certificate::from_pem(data).map_err(Error::LoadCert)?;
|
||||
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a writer.
|
||||
pub fn write_pem_cert(mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF)?;
|
||||
pub fn write_pem_cert(path: &Path, mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF).map_err(Error::SaveCert)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a file.
|
||||
pub fn read_pem_cert_file(path: &Path) -> Result<Certificate> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_cert(reader)
|
||||
read_pem_cert(path, reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a file.
|
||||
pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_cert(writer, cert)
|
||||
write_pem_cert(path, writer, cert)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a reader.
|
||||
pub fn read_pem_public_key(path: &Path, mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
let mut data = String::new();
|
||||
reader
|
||||
.read_to_string(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
let key = RsaPublicKey::from_public_key_pem(&data).map_err(Error::LoadPubKey)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a writer.
|
||||
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key.to_public_key_pem(LineEnding::LF)?;
|
||||
pub fn write_pem_public_key(path: &Path, mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.map_err(Error::SavePubKey)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a file.
|
||||
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_public_key(path, reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
write_pem_public_key(path, writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
pub fn read_pem_key(
|
||||
path: &Path,
|
||||
mut reader: impl Read,
|
||||
source: &PassphraseSource,
|
||||
) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
reader
|
||||
.read_to_string(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
if data.contains("ENCRYPTED") {
|
||||
let passphrase = source.acquire(false)?;
|
||||
@@ -258,6 +573,7 @@ pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<
|
||||
|
||||
/// Write PEM-encoded PKCS8 private key to a writer.
|
||||
pub fn write_pem_key(
|
||||
path: &Path,
|
||||
mut writer: impl Write,
|
||||
key: &RsaPrivateKey,
|
||||
source: &PassphraseSource,
|
||||
@@ -300,20 +616,24 @@ pub fn write_pem_key(
|
||||
.encrypt_with_params(pbes2_params, passphrase)
|
||||
.map_err(Error::SaveKeyEncrypted)?;
|
||||
|
||||
secret_doc.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)?
|
||||
secret_doc
|
||||
.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)
|
||||
.map_err(pkcs8::Error::Asn1)
|
||||
.map_err(Error::SaveKeyEncrypted)?
|
||||
};
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a file.
|
||||
pub fn read_pem_key_file(path: &Path, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_key(reader, source)
|
||||
read_pem_key(path, reader, source)
|
||||
}
|
||||
|
||||
/// Save PEM-encoded PKCS8 private key to a file.
|
||||
@@ -333,22 +653,24 @@ pub fn write_pem_key_file(
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let file = options.open(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = options
|
||||
.open(path)
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_key(writer, key, source)
|
||||
write_pem_key(path, writer, key, source)
|
||||
}
|
||||
|
||||
/// Get the RSA public key from a certificate.
|
||||
pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
|
||||
let public_key =
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())?;
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())
|
||||
.map_err(Error::LoadPubKey)?;
|
||||
|
||||
Ok(public_key)
|
||||
}
|
||||
|
||||
/// Check if a certificate matches a private key.
|
||||
pub fn cert_matches_key(cert: &Certificate, key: &RsaPrivateKey) -> Result<bool> {
|
||||
pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool> {
|
||||
let public_key = get_public_key(cert)?;
|
||||
|
||||
Ok(key.to_public_key() == public_key)
|
||||
@@ -356,27 +678,26 @@ pub fn cert_matches_key(cert: &Certificate, key: &RsaPrivateKey) -> Result<bool>
|
||||
|
||||
/// Parse a CMS [`SignedData`] structure from raw DER-encoded data.
|
||||
pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
|
||||
let ci = ContentInfo::from_der(data)?;
|
||||
let sd = ci.content.decode_as::<SignedData>()?;
|
||||
let ci = ContentInfo::from_der(data).map_err(Error::CmsParse)?;
|
||||
let sd = ci
|
||||
.content
|
||||
.decode_as::<SignedData>()
|
||||
.map_err(Error::CmsParse)?;
|
||||
|
||||
Ok(sd)
|
||||
}
|
||||
|
||||
/// Get a list of all standard X509 certificates contained within a
|
||||
/// Get an iterator to all standard X509 certificates contained within a
|
||||
/// [`SignedData`] structure.
|
||||
pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
|
||||
sd.certificates.as_ref().map_or_else(Vec::new, |certs| {
|
||||
certs
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
pub fn iter_cms_certs(sd: &SignedData) -> impl Iterator<Item = &Certificate> {
|
||||
sd.certificates.iter().flat_map(|certs| {
|
||||
certs.0.iter().filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -386,12 +707,11 @@ pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
|
||||
/// a transport mechanism for a raw signature. Thus, we need to ensure that the
|
||||
/// signature covers nothing but the raw data.
|
||||
pub fn cms_sign_external(
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
digest: &[u8],
|
||||
) -> Result<ContentInfo> {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
let signature = key.sign(scheme, digest)?;
|
||||
let signature = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
|
||||
|
||||
let digest_algorithm = AlgorithmIdentifierOwned {
|
||||
oid: const_oid::db::rfc5912::ID_SHA_256,
|
||||
@@ -400,14 +720,16 @@ pub fn cms_sign_external(
|
||||
|
||||
let signed_data = SignedData {
|
||||
version: CmsVersion::V1,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])?,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])
|
||||
.map_err(Error::CmsSign)?,
|
||||
encap_content_info: EncapsulatedContentInfo {
|
||||
econtent_type: const_oid::db::rfc5911::ID_DATA,
|
||||
econtent: None,
|
||||
},
|
||||
certificates: Some(CertificateSet::try_from(vec![
|
||||
CertificateChoices::Certificate(cert.clone()),
|
||||
])?),
|
||||
certificates: Some(
|
||||
CertificateSet::try_from(vec![CertificateChoices::Certificate(cert.clone())])
|
||||
.map_err(Error::CmsSign)?,
|
||||
),
|
||||
crls: None,
|
||||
signer_infos: SignerInfos::try_from(vec![SignerInfo {
|
||||
version: CmsVersion::V1,
|
||||
@@ -421,14 +743,15 @@ pub fn cms_sign_external(
|
||||
oid: const_oid::db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
|
||||
parameters: None,
|
||||
},
|
||||
signature: SignatureValue::new(signature)?,
|
||||
signature: SignatureValue::new(signature).map_err(Error::CmsSign)?,
|
||||
unsigned_attrs: None,
|
||||
}])?,
|
||||
}])
|
||||
.map_err(Error::CmsSign)?,
|
||||
};
|
||||
|
||||
let signed_data = ContentInfo {
|
||||
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
|
||||
content: Any::encode_from(&signed_data)?,
|
||||
content: Any::encode_from(&signed_data).map_err(Error::CmsSign)?,
|
||||
};
|
||||
|
||||
Ok(signed_data)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
use bstr::{ByteSlice, ByteVec};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use serde::{Deserializer, Serializer, de::Visitor};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
@@ -68,7 +66,7 @@ where
|
||||
{
|
||||
struct EscapedStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
|
||||
impl<T> Visitor<'_> for EscapedStrVisitor<T>
|
||||
where
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
|
||||
+940
-655
File diff suppressed because it is too large
Load Diff
+658
-444
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
stream::{Check, Stream},
|
||||
@@ -16,6 +13,8 @@ use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::stream::ReadFixedSizeExt;
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
@@ -24,10 +23,12 @@ static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("I/O error when autodetecting compression format")]
|
||||
AutoDetect(#[source] io::Error),
|
||||
#[error("Failed to initialize legacy LZ4 encoder")]
|
||||
Lz4Init(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -61,7 +62,7 @@ impl<W: Write> Lz4LegacyEncoder<W> {
|
||||
let compressed = lz4_flex::block::compress(&self.buf[..self.n_filled]);
|
||||
|
||||
let writer = self.writer.as_mut().unwrap();
|
||||
writer.write_u32::<LittleEndian>(compressed.len() as u32)?;
|
||||
writer.write_all(&(compressed.len() as u32).to_le_bytes())?;
|
||||
writer.write_all(&compressed)?;
|
||||
|
||||
self.n_filled = 0;
|
||||
@@ -122,10 +123,9 @@ pub enum CompressedReader<R: Read> {
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
let magic = reader.read_array_exact::<6>().map_err(Error::AutoDetect)?;
|
||||
|
||||
reader.rewind()?;
|
||||
reader.rewind().map_err(Error::AutoDetect)?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
@@ -184,10 +184,13 @@ impl<W: Write> CompressedWriter<W> {
|
||||
CompressedFormat::Gzip => {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||
CompressedFormat::Lz4Legacy => {
|
||||
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
|
||||
Ok(Self::Lz4Legacy(encoder))
|
||||
}
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32).map_err(Error::XzInit)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
}
|
||||
}
|
||||
|
||||
+166
-108
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -15,6 +13,8 @@ use bstr::ByteSlice;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
escape,
|
||||
@@ -50,55 +50,94 @@ const VEC_CAP_THRESHOLD: usize = 16384;
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
UnknownMagic([u8; 6]),
|
||||
#[error("Path is not NULL-terminated: {:?}", .0.as_bstr())]
|
||||
PathNotNullTerminated(Vec<u8>),
|
||||
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
|
||||
HardLinksNotSupported(Vec<u8>),
|
||||
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
|
||||
#[error("Entry of type {0} should not have data: {path:?}", path = .1.as_bstr())]
|
||||
EntryHasData(CpioEntryType, Vec<u8>),
|
||||
#[error("No inodes available for device {0:x},{1:x}")]
|
||||
DeviceFull(u32, u32),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("No inodes available for device {major:x},{minor:x}")]
|
||||
DeviceFull { major: u32, minor: u32 },
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("{0:?} contains invalid hex integer")]
|
||||
InvalidHexInt(&'static str, #[source] InvalidHexCharError),
|
||||
#[error("Failed to read cpio data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write cpio data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Read u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn read_int(mut reader: impl Read) -> io::Result<u32> {
|
||||
let mut buf = [0u8; 8];
|
||||
reader.read_exact(&mut buf)?;
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0:?}: Invalid hex char: {1:?}")]
|
||||
pub struct InvalidHexCharError(RawHexU32, char);
|
||||
|
||||
let mut value = 0;
|
||||
/// ASCII-encoded hex integer value used in cpio header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHexU32([u8; 8]);
|
||||
|
||||
for b in buf {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
|
||||
)
|
||||
})?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
impl fmt::Debug for RawHexU32 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0.as_bstr())
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Write u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<u32> for RawHexU32 {
|
||||
fn from(mut value: u32) -> Self {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
Self(buf)
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_all(&buf)
|
||||
impl TryFrom<RawHexU32> for u32 {
|
||||
type Error = InvalidHexCharError;
|
||||
|
||||
fn try_from(raw_value: RawHexU32) -> std::result::Result<Self, Self::Error> {
|
||||
let mut value = 0;
|
||||
|
||||
for b in raw_value.0 {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or(InvalidHexCharError(raw_value, c))?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the cpio header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
|
||||
magic: [u8; 6],
|
||||
inode: RawHexU32,
|
||||
mode: RawHexU32,
|
||||
uid: RawHexU32,
|
||||
gid: RawHexU32,
|
||||
nlink: RawHexU32,
|
||||
mtime: RawHexU32,
|
||||
file_size: RawHexU32,
|
||||
dev_maj: RawHexU32,
|
||||
dev_min: RawHexU32,
|
||||
rdev_maj: RawHexU32,
|
||||
rdev_min: RawHexU32,
|
||||
path_size: RawHexU32,
|
||||
crc32: RawHexU32,
|
||||
}
|
||||
|
||||
/// Read a chunk of bytes from the reader. If `size` is less than
|
||||
@@ -213,17 +252,14 @@ impl CpioEntryData {
|
||||
pub fn size(&self) -> Result<u32> {
|
||||
let size = match self {
|
||||
Self::Size(s) => *s,
|
||||
Self::Data(d) => d
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("data_size"))?,
|
||||
Self::Data(d) => d.len().to_u32().ok_or(Error::IntOverflow("data_size"))?,
|
||||
};
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn is_size(&self) -> bool {
|
||||
matches!(self, CpioEntryData::Size(_))
|
||||
matches!(self, Self::Size(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,41 +427,45 @@ impl<R: Read> FromReader<R> for CpioEntry {
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
let mut reader = CountingReader::new(reader);
|
||||
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if magic != *MAGIC_NEW && magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
if header.magic != *MAGIC_NEW && header.magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(header.magic));
|
||||
}
|
||||
|
||||
let inode = read_int(&mut reader)?;
|
||||
let mode = read_int(&mut reader)?;
|
||||
let uid = read_int(&mut reader)?;
|
||||
let gid = read_int(&mut reader)?;
|
||||
let nlink = read_int(&mut reader)?;
|
||||
let mtime = read_int(&mut reader)?;
|
||||
let file_size = read_int(&mut reader)?;
|
||||
let dev_maj = read_int(&mut reader)?;
|
||||
let dev_min = read_int(&mut reader)?;
|
||||
let rdev_maj = read_int(&mut reader)?;
|
||||
let rdev_min = read_int(&mut reader)?;
|
||||
let path_size = read_int(&mut reader)?;
|
||||
let crc32 = read_int(&mut reader)?;
|
||||
macro_rules! get_field {
|
||||
($name:ident) => {
|
||||
let $name = u32::try_from(header.$name)
|
||||
.map_err(|e| Error::InvalidHexInt(stringify!($name), e))?;
|
||||
};
|
||||
}
|
||||
|
||||
get_field!(inode);
|
||||
get_field!(mode);
|
||||
get_field!(uid);
|
||||
get_field!(gid);
|
||||
get_field!(nlink);
|
||||
get_field!(mtime);
|
||||
get_field!(file_size);
|
||||
get_field!(dev_maj);
|
||||
get_field!(dev_min);
|
||||
get_field!(rdev_maj);
|
||||
get_field!(rdev_min);
|
||||
get_field!(path_size);
|
||||
get_field!(crc32);
|
||||
|
||||
let mut path = read_data(
|
||||
&mut reader,
|
||||
path_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("path", e))?;
|
||||
if path.last() != Some(&b'\0') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Filename is not NULL-terminated",
|
||||
)
|
||||
.into());
|
||||
return Err(Error::PathNotNullTerminated(path));
|
||||
}
|
||||
path.pop();
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
padding::read_discard(&mut reader, 4).map_err(|e| Error::DataRead("path_padding", e))?;
|
||||
|
||||
let file_type = CpioEntryType::from_mode(mode);
|
||||
let data = match file_type {
|
||||
@@ -437,8 +477,10 @@ impl<R: Read> FromReader<R> for CpioEntry {
|
||||
&mut reader,
|
||||
file_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("content", e))?;
|
||||
padding::read_discard(&mut reader, 4)
|
||||
.map_err(|e| Error::DataRead("content_padding", e))?;
|
||||
|
||||
CpioEntryData::Data(content)
|
||||
}
|
||||
@@ -477,7 +519,7 @@ impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.and_then(|s| s.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
|
||||
.ok_or(Error::IntOverflow("path_size"))?;
|
||||
|
||||
let file_size = self.data.size()?;
|
||||
if file_size != 0
|
||||
@@ -487,35 +529,47 @@ impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
return Err(Error::EntryHasData(self.file_type, self.path.clone()));
|
||||
}
|
||||
|
||||
if self.crc32 == 0 {
|
||||
writer.write_all(MAGIC_NEW)?;
|
||||
} else {
|
||||
writer.write_all(MAGIC_NEW_CRC)?;
|
||||
}
|
||||
|
||||
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
|
||||
|
||||
write_int(&mut writer, self.inode)?;
|
||||
write_int(&mut writer, mode)?;
|
||||
write_int(&mut writer, self.uid)?;
|
||||
write_int(&mut writer, self.gid)?;
|
||||
write_int(&mut writer, self.nlink)?;
|
||||
write_int(&mut writer, self.mtime)?;
|
||||
write_int(&mut writer, file_size)?;
|
||||
write_int(&mut writer, self.dev_maj)?;
|
||||
write_int(&mut writer, self.dev_min)?;
|
||||
write_int(&mut writer, self.rdev_maj)?;
|
||||
write_int(&mut writer, self.rdev_min)?;
|
||||
write_int(&mut writer, path_size)?;
|
||||
write_int(&mut writer, self.crc32)?;
|
||||
let raw_header = RawHeader {
|
||||
magic: if self.crc32 == 0 {
|
||||
*MAGIC_NEW
|
||||
} else {
|
||||
*MAGIC_NEW_CRC
|
||||
},
|
||||
inode: self.inode.into(),
|
||||
mode: mode.into(),
|
||||
uid: self.uid.into(),
|
||||
gid: self.gid.into(),
|
||||
nlink: self.nlink.into(),
|
||||
mtime: self.mtime.into(),
|
||||
file_size: file_size.into(),
|
||||
dev_maj: self.dev_maj.into(),
|
||||
dev_min: self.dev_min.into(),
|
||||
rdev_maj: self.rdev_maj.into(),
|
||||
rdev_min: self.rdev_min.into(),
|
||||
path_size: path_size.into(),
|
||||
crc32: self.crc32.into(),
|
||||
};
|
||||
|
||||
writer.write_all(&self.path)?;
|
||||
writer.write_zeros_exact(1)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
raw_header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
writer
|
||||
.write_all(&self.path)
|
||||
.map_err(|e| Error::DataWrite("path", e))?;
|
||||
writer
|
||||
.write_zeros_exact(1)
|
||||
.map_err(|e| Error::DataWrite("path", e))?;
|
||||
padding::write_zeros(&mut writer, 4).map_err(|e| Error::DataWrite("path_padding", e))?;
|
||||
|
||||
if let CpioEntryData::Data(d) = &self.data {
|
||||
writer.write_all(d)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
writer
|
||||
.write_all(d)
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
padding::write_zeros(&mut writer, 4)
|
||||
.map_err(|e| Error::DataWrite("content_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -559,7 +613,8 @@ impl<R: Read> CpioReader<R> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.skip_data()?;
|
||||
self.skip_data()
|
||||
.map_err(|e| Error::DataRead("content", e))?;
|
||||
|
||||
let entry = CpioEntry::from_reader(&mut self.reader)?;
|
||||
|
||||
@@ -624,7 +679,8 @@ impl<W: Write> CpioWriter<W> {
|
||||
}
|
||||
|
||||
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
|
||||
self.finish_entry()?;
|
||||
self.finish_entry()
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
|
||||
entry.to_writer(&mut self.writer)?;
|
||||
|
||||
@@ -638,13 +694,15 @@ impl<W: Write> CpioWriter<W> {
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.finish_entry()?;
|
||||
self.finish_entry()
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
|
||||
self.start_entry(&CpioEntry::new_trailer())?;
|
||||
|
||||
// Pad until the end of the block.
|
||||
if self.pad_to_block_size {
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)?;
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)
|
||||
.map_err(|e| Error::DataWrite("block_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(self.writer.finish().0)
|
||||
@@ -682,14 +740,15 @@ pub fn load(
|
||||
let mut entries = vec![];
|
||||
|
||||
while let Some(mut entry) = cpio_reader.next_entry()? {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("entry", e))?;
|
||||
|
||||
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(entry.path.clone()));
|
||||
return Err(Error::HardLinksNotSupported(entry.path));
|
||||
}
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)
|
||||
.map_err(|e| Error::DataWrite("data", e))?;
|
||||
entry.data = CpioEntryData::Data(data);
|
||||
}
|
||||
|
||||
@@ -712,11 +771,7 @@ pub fn sort(entries: &mut [CpioEntry]) {
|
||||
/// 300000.
|
||||
pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()> {
|
||||
fn next_non_zero(i: u32) -> u32 {
|
||||
if i == u32::MAX {
|
||||
1
|
||||
} else {
|
||||
i.wrapping_add(1)
|
||||
}
|
||||
if i == u32::MAX { 1 } else { i.wrapping_add(1) }
|
||||
}
|
||||
|
||||
// (dev maj, dev min) -> (inode set, last assigned inode)
|
||||
@@ -745,7 +800,10 @@ pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()
|
||||
|
||||
while set.contains(&unused) {
|
||||
if unused == *last {
|
||||
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
|
||||
return Err(Error::DeviceFull {
|
||||
major: entry.dev_maj,
|
||||
minor: entry.dev_min,
|
||||
});
|
||||
}
|
||||
|
||||
unused = next_non_zero(unused);
|
||||
@@ -769,7 +827,7 @@ pub fn save(
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
for entry in entries {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataWrite("entry", e))?;
|
||||
|
||||
cpio_writer.start_entry(entry)?;
|
||||
// CpioEntryData::Data will have already been written.
|
||||
|
||||
+134
-99
@@ -1,33 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
prelude::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::{ParallelSlice, ParallelSliceMut},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{FromBytes, IntoBytes, little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
util::{self, NumBytes},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
// Not to be confused with the 255-byte RS block size.
|
||||
const FEC_BLOCK_SIZE: usize = 4096;
|
||||
const FEC_HEADER_SIZE: usize = 60;
|
||||
const FEC_MAGIC: u32 = 0xFECFECFE;
|
||||
const FEC_VERSION: u32 = 0;
|
||||
|
||||
@@ -66,9 +65,17 @@ pub enum Error {
|
||||
#[error("Expected FEC digest {expected}, but have {actual}")]
|
||||
InvalidFecDigest { expected: String, actual: String },
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to reopen output file")]
|
||||
OutputReopen(#[source] io::Error),
|
||||
#[error("Failed to read FEC data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write FEC data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -96,7 +103,7 @@ impl Codeword {
|
||||
&mut self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn parity(&mut self) -> &[u8] {
|
||||
fn parity(&self) -> &[u8] {
|
||||
&self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
@@ -157,28 +164,29 @@ impl Fec {
|
||||
input: file_size,
|
||||
block: block_size,
|
||||
});
|
||||
} else if block_size > FEC_MAX_BLOCK_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("block_size"));
|
||||
}
|
||||
|
||||
util::check_bounds(block_size, ..=FEC_MAX_BLOCK_SIZE)
|
||||
.map_err(|e| Error::IntOutOfBounds("block_size", e))?;
|
||||
|
||||
let rs_k = 255 - parity;
|
||||
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
|
||||
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
|
||||
.checked_mul(u64::from(parity))
|
||||
.and_then(|s| s.checked_mul(u64::from(block_size)))
|
||||
.and_then(|s| s.to_usize())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_size"))?;
|
||||
.ok_or(Error::IntOverflow("fec_data_size"))?;
|
||||
rounds
|
||||
.checked_mul(u64::from(rs_k))
|
||||
.and_then(|s| s.checked_mul(u64::from(block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_grid_size"))?;
|
||||
.ok_or(Error::IntOverflow("fec_grid_size"))?;
|
||||
|
||||
Ok(Self {
|
||||
file_size,
|
||||
@@ -196,7 +204,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
|
||||
}
|
||||
|
||||
@@ -212,9 +220,8 @@ impl Fec {
|
||||
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > self.file_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
util::check_bounds(last.end, ..=self.file_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
@@ -225,7 +232,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 {
|
||||
@@ -363,7 +370,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let encode = verityrs::FN_ENCODE[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
@@ -384,7 +393,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
@@ -414,7 +425,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let mut grid = self.read_round(reader, round)?;
|
||||
let mut grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
let mut num_corrected = 0;
|
||||
@@ -432,7 +445,8 @@ impl Fec {
|
||||
}
|
||||
|
||||
if num_corrected > 0 {
|
||||
self.write_round(writer, round, &grid)?;
|
||||
self.write_round(writer, round, &grid)
|
||||
.map_err(|e| Error::DataWrite("round", e))?;
|
||||
}
|
||||
|
||||
Ok(num_corrected)
|
||||
@@ -453,9 +467,9 @@ impl Fec {
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -488,9 +502,9 @@ impl Fec {
|
||||
.enumerate()
|
||||
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -521,9 +535,9 @@ impl Fec {
|
||||
fec.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -562,10 +576,10 @@ impl Fec {
|
||||
.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
|
||||
self.repair_one_round(reader, writer, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
@@ -576,6 +590,26 @@ impl Fec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the FEC image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`FEC_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
/// Image version. This should be equal to [`FEC_VERSION`].
|
||||
version: little_endian::U32,
|
||||
/// Size of this [`RawHeader`].
|
||||
header_size: little_endian::U32,
|
||||
/// Number of parity bytes per 255-byte Reed-Solomon codeword.
|
||||
parity: little_endian::U32,
|
||||
/// Size of the FEC data.
|
||||
fec_size: little_endian::U32,
|
||||
/// Size of the actual data.
|
||||
data_size: little_endian::U64,
|
||||
/// SHA-256 digest of the FEC data.
|
||||
digest: [u8; 32],
|
||||
}
|
||||
|
||||
/// A type for reading and writing AOSP's standalone FEC image format.
|
||||
///
|
||||
/// The FEC data parser in this implementation is strict. All header fields,
|
||||
@@ -606,10 +640,10 @@ impl FecImage {
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let data_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
|
||||
@@ -669,26 +703,23 @@ impl FecImage {
|
||||
|
||||
/// Build one instance of the FEC header. The caller is responsible for
|
||||
/// writing it to both of the header locations at the end of the file.
|
||||
fn build_header(&self) -> Result<[u8; FEC_HEADER_SIZE]> {
|
||||
let fec_size = self
|
||||
.fec
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
|
||||
|
||||
let mut writer = Cursor::new([0u8; FEC_HEADER_SIZE]);
|
||||
fn build_header(&self) -> Result<RawHeader> {
|
||||
let fec_size: u32 =
|
||||
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
|
||||
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
|
||||
|
||||
writer.write_u32::<LittleEndian>(FEC_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(FEC_VERSION)?;
|
||||
writer.write_u32::<LittleEndian>(FEC_HEADER_SIZE as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.parity.into())?;
|
||||
writer.write_u32::<LittleEndian>(fec_size)?;
|
||||
writer.write_u64::<LittleEndian>(self.data_size)?;
|
||||
writer.write_all(digest.as_ref())?;
|
||||
let header = RawHeader {
|
||||
magic: FEC_MAGIC.into(),
|
||||
version: FEC_VERSION.into(),
|
||||
header_size: (mem::size_of::<RawHeader>() as u32).into(),
|
||||
parity: u32::from(self.parity).into(),
|
||||
fec_size: fec_size.into(),
|
||||
data_size: self.data_size.into(),
|
||||
digest: digest.as_ref().try_into().unwrap(),
|
||||
};
|
||||
|
||||
Ok(writer.into_inner())
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,48 +730,47 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
// Avoid requiring seekable readers since we need to read everything
|
||||
// into memory anyway.
|
||||
let mut fec = Vec::new();
|
||||
reader.read_to_end(&mut fec)?;
|
||||
reader
|
||||
.read_to_end(&mut fec)
|
||||
.map_err(|e| Error::DataRead("fec", e))?;
|
||||
|
||||
if fec.len() < FEC_BLOCK_SIZE {
|
||||
return Err(Error::DataTooSmall);
|
||||
}
|
||||
|
||||
// Make sure both headers match.
|
||||
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
|
||||
let header2_offset = fec.len() - FEC_HEADER_SIZE;
|
||||
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
|
||||
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
|
||||
|
||||
let (header, _) =
|
||||
RawHeader::ref_from_prefix(&fec[header1_offset..]).map_err(|_| Error::DataTooSmall)?;
|
||||
let header_size = header.header_size.get() as usize;
|
||||
|
||||
if header_size > FEC_BLOCK_SIZE / 2 {
|
||||
// ref_from_prefix() already handles the "too small" case.
|
||||
return Err(Error::InvalidHeaderSize(header.header_size.get()));
|
||||
}
|
||||
|
||||
let header2_offset = fec.len() - header_size;
|
||||
|
||||
// Make sure both headers match, accounting for potential custom fields.
|
||||
let header1_raw = &fec[header1_offset..][..header_size];
|
||||
let header2_raw = &fec[header2_offset..][..header_size];
|
||||
|
||||
if header1_raw != header2_raw {
|
||||
return Err(Error::HeadersDifferent);
|
||||
}
|
||||
|
||||
let mut header_reader = Cursor::new(header1_raw);
|
||||
|
||||
let magic = header_reader.read_u32::<LittleEndian>()?;
|
||||
if magic != FEC_MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
if header.magic != FEC_MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(header.magic.get()));
|
||||
}
|
||||
|
||||
let version = header_reader.read_u32::<LittleEndian>()?;
|
||||
if version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(version));
|
||||
if header.version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(header.version.get()));
|
||||
}
|
||||
|
||||
let header_size = header_reader.read_u32::<LittleEndian>()?;
|
||||
if header_size != FEC_HEADER_SIZE as u32 {
|
||||
return Err(Error::InvalidHeaderSize(header_size));
|
||||
}
|
||||
let parity: u8 =
|
||||
util::try_cast(header.parity.get()).map_err(|e| Error::IntOutOfBounds("parity", e))?;
|
||||
|
||||
let parity = header_reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_u8()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("parity"))?;
|
||||
|
||||
let fec_size = header_reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
|
||||
let fec_size = header.fec_size.get() as usize;
|
||||
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
|
||||
if fec_size != actual_fec_size {
|
||||
return Err(Error::InvalidHeaderFecSize {
|
||||
@@ -749,25 +779,22 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
});
|
||||
}
|
||||
|
||||
let input_size = header_reader.read_u64::<LittleEndian>()?;
|
||||
let data_size = header.data_size.get();
|
||||
|
||||
let mut digest = [0u8; 32];
|
||||
header_reader.read_exact(&mut digest)?;
|
||||
|
||||
// Chop off headers.
|
||||
fec.resize(fec_size, 0);
|
||||
|
||||
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec);
|
||||
if digest != actual_digest.as_ref() {
|
||||
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec[..fec_size]);
|
||||
if header.digest != actual_digest.as_ref() {
|
||||
return Err(Error::InvalidFecDigest {
|
||||
expected: hex::encode(digest),
|
||||
expected: hex::encode(header.digest),
|
||||
actual: hex::encode(actual_digest),
|
||||
});
|
||||
}
|
||||
|
||||
// Chop off headers.
|
||||
fec.resize(fec_size, 0);
|
||||
|
||||
Ok(Self {
|
||||
fec,
|
||||
data_size: input_size,
|
||||
data_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
@@ -779,10 +806,18 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let header = self.build_header()?;
|
||||
|
||||
writer.write_all(&self.fec)?;
|
||||
writer.write_all(&header)?;
|
||||
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
|
||||
writer.write_all(&header)?;
|
||||
writer
|
||||
.write_all(&self.fec)
|
||||
.map_err(|e| Error::DataWrite("fec_data", e))?;
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("fec_header_1", e))?;
|
||||
writer
|
||||
.write_zeros_exact((FEC_BLOCK_SIZE - 2 * header.as_bytes().len()) as u64)
|
||||
.map_err(|e| Error::DataWrite("fec_header_padding", e))?;
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("fec_header_2", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -791,8 +826,8 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Seek,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
io::{Cursor, Seek},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
+162
-119
@@ -1,29 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
ops::Range,
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{FromBytes, IntoBytes, little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::{avb, padding},
|
||||
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
|
||||
util::{self, NumBytes},
|
||||
format::{
|
||||
avb,
|
||||
padding::{self, ZeroPadding},
|
||||
},
|
||||
stream::{self, FromReader, ReadFixedSizeExt, ReadSeekReopen, ToWriter},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -42,28 +44,37 @@ pub enum Error {
|
||||
InvalidHeaderMagic([u8; 16]),
|
||||
#[error("Invalid hash tree header version: {0}")]
|
||||
InvalidHeaderVersion(u16),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("Hashing algorithm not supported: {:?}", .0.as_bstr())]
|
||||
UnsupportedHashAlgorithm(Vec<u8>),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to compute hash tree of input file")]
|
||||
InputDigest(#[source] io::Error),
|
||||
#[error("Failed to read hash tree data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write hash tree data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::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,23 +82,23 @@ 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)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
.ok_or(Error::IntOverflow("level_size"))?;
|
||||
|
||||
// Depending on the chosen block size, the original file size could
|
||||
// overflow a usize without the first level's size doing the same.
|
||||
let level_size_usize = level_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
let level_size_usize: usize =
|
||||
util::try_cast(level_size).map_err(|e| Error::IntOutOfBounds("level_size", e))?;
|
||||
|
||||
ranges.push(0..level_size_usize);
|
||||
}
|
||||
@@ -109,9 +120,8 @@ impl<'a> HashTree<'a> {
|
||||
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > image_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
util::check_bounds(last.end, ..=image_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
@@ -122,7 +132,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 +153,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 +167,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
|
||||
@@ -186,12 +195,13 @@ impl<'a> HashTree<'a> {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
assert!(
|
||||
size > self.block_size as u64,
|
||||
size > u64::from(self.block_size),
|
||||
"Images smaller than block size must use a normal hash",
|
||||
);
|
||||
|
||||
// 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 +232,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)
|
||||
@@ -259,12 +270,12 @@ impl<'a> HashTree<'a> {
|
||||
) -> Result<Vec<u8>> {
|
||||
// Small files are hashed directly.
|
||||
if image_size <= u64::from(self.block_size) {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; image_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let buf = reader
|
||||
.read_vec_exact(image_size as usize)
|
||||
.map_err(Error::InputDigest)?;
|
||||
|
||||
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();
|
||||
|
||||
@@ -287,7 +298,8 @@ impl<'a> HashTree<'a> {
|
||||
prev_size as u64,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::InputDigest)?;
|
||||
} else if let Some(r) = ranges {
|
||||
// Read partial blocks from file.
|
||||
let block_ranges = self.blocks_for_ranges(image_size, r)?;
|
||||
@@ -298,10 +310,12 @@ impl<'a> HashTree<'a> {
|
||||
&block_ranges,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::InputDigest)?;
|
||||
} else {
|
||||
// Read entire file.
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)
|
||||
.map_err(Error::InputDigest)?;
|
||||
}
|
||||
|
||||
// No need to explicitly ensure the level is padded to the block
|
||||
@@ -309,8 +323,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 +339,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.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
let mut hash_tree_data = vec![0u8; hash_tree_size];
|
||||
|
||||
let root_digest = self.calculate(
|
||||
@@ -352,7 +365,7 @@ impl<'a> HashTree<'a> {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
@@ -381,7 +394,7 @@ impl<'a> HashTree<'a> {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
@@ -402,8 +415,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),
|
||||
@@ -415,6 +429,28 @@ impl<'a> HashTree<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for our custom hash tree image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
|
||||
magic: [u8; 16],
|
||||
/// Image version. This should be equal to [`HashTreeImage::VERSION`].
|
||||
version: little_endian::U16,
|
||||
/// Size of the actual data.
|
||||
image_size: little_endian::U64,
|
||||
/// Block size.
|
||||
block_size: little_endian::U32,
|
||||
/// Hash algorithm.
|
||||
algorithm: [u8; 16],
|
||||
/// Salt size.
|
||||
salt_size: little_endian::U16,
|
||||
/// Root digest size.
|
||||
root_digest_size: little_endian::U16,
|
||||
/// Hash tree size.
|
||||
hash_tree_size: little_endian::U32,
|
||||
}
|
||||
|
||||
/// A type for reading and writing a custom hash tree image format.
|
||||
///
|
||||
/// File format:
|
||||
@@ -442,6 +478,7 @@ pub struct HashTreeImage {
|
||||
impl fmt::Debug for HashTreeImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HashTreeImage")
|
||||
.field("image_size", &self.image_size)
|
||||
.field("block_size", &self.block_size)
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("salt", &hex::encode(&self.salt))
|
||||
@@ -455,9 +492,9 @@ impl HashTreeImage {
|
||||
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::ring_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
|
||||
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::digest_algorithm(name)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
@@ -468,12 +505,12 @@ impl HashTreeImage {
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let image_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let ring_algorithm = Self::ring_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
|
||||
let image_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let digest_algorithm = Self::digest_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -493,8 +530,8 @@ impl HashTreeImage {
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
self.root_digest = hash_tree.update(
|
||||
input,
|
||||
@@ -513,8 +550,8 @@ impl HashTreeImage {
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
hash_tree.verify(
|
||||
input,
|
||||
@@ -530,40 +567,37 @@ impl<R: Read> FromReader<R> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
let mut magic = [0u8; 16];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if header.magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(header.magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u16::<LittleEndian>()?;
|
||||
if version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(version));
|
||||
if header.version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(header.version.get()));
|
||||
}
|
||||
|
||||
let image_size = reader.read_u64::<LittleEndian>()?;
|
||||
let block_size = reader.read_u32::<LittleEndian>()?;
|
||||
let algorithm = reader.read_string_padded(16)?;
|
||||
let salt_size = reader.read_u16::<LittleEndian>()?;
|
||||
let root_digest_size = reader.read_u16::<LittleEndian>()?;
|
||||
let hash_tree_size = reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
let algorithm = header.algorithm.trim_end_padding();
|
||||
let algorithm = str::from_utf8(algorithm)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(algorithm.to_vec()))?;
|
||||
|
||||
let mut salt = vec![0u8; usize::from(salt_size)];
|
||||
reader.read_exact(&mut salt)?;
|
||||
let salt = reader
|
||||
.read_vec_exact(usize::from(header.salt_size))
|
||||
.map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
|
||||
reader.read_exact(&mut root_digest)?;
|
||||
let root_digest = reader
|
||||
.read_vec_exact(usize::from(header.root_digest_size))
|
||||
.map_err(|e| Error::DataRead("root_digest", e))?;
|
||||
|
||||
let mut hash_tree = vec![0u8; hash_tree_size];
|
||||
reader.read_exact(&mut hash_tree)?;
|
||||
let hash_tree = reader
|
||||
.read_vec_exact(header.hash_tree_size.get() as usize)
|
||||
.map_err(|e| Error::DataRead("hash_tree", e))?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
image_size: header.image_size.get(),
|
||||
block_size: header.block_size.get(),
|
||||
algorithm: algorithm.to_owned(),
|
||||
salt,
|
||||
root_digest,
|
||||
hash_tree,
|
||||
@@ -575,33 +609,42 @@ impl<W: Write> ToWriter<W> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let salt_size = self
|
||||
.salt
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
|
||||
let root_digest_size = self
|
||||
.root_digest
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
|
||||
let hash_tree_size = self
|
||||
.hash_tree
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
let algorithm = self
|
||||
.algorithm
|
||||
.as_bytes()
|
||||
.to_padded_array::<16>()
|
||||
.ok_or_else(|| Error::UnsupportedHashAlgorithm(self.algorithm.as_bytes().to_vec()))?;
|
||||
|
||||
writer.write_all(Self::MAGIC)?;
|
||||
writer.write_u16::<LittleEndian>(Self::VERSION)?;
|
||||
writer.write_u64::<LittleEndian>(self.image_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.block_size)?;
|
||||
writer.write_string_padded(&self.algorithm, 16)?;
|
||||
writer.write_u16::<LittleEndian>(salt_size)?;
|
||||
writer.write_u16::<LittleEndian>(root_digest_size)?;
|
||||
writer.write_u32::<LittleEndian>(hash_tree_size)?;
|
||||
writer.write_all(&self.salt)?;
|
||||
writer.write_all(&self.root_digest)?;
|
||||
writer.write_all(&self.hash_tree)?;
|
||||
let salt_size: u16 =
|
||||
util::try_cast(self.salt.len()).map_err(|e| Error::IntOutOfBounds("salt_size", e))?;
|
||||
let root_digest_size: u16 = util::try_cast(self.root_digest.len())
|
||||
.map_err(|e| Error::IntOutOfBounds("root_digest_size", e))?;
|
||||
let hash_tree_size: u32 = util::try_cast(self.hash_tree.len())
|
||||
.map_err(|e| Error::IntOutOfBounds("hash_tree_size", e))?;
|
||||
|
||||
let header = RawHeader {
|
||||
magic: *Self::MAGIC,
|
||||
version: Self::VERSION.into(),
|
||||
image_size: self.image_size.into(),
|
||||
block_size: self.block_size.into(),
|
||||
algorithm,
|
||||
salt_size: salt_size.into(),
|
||||
root_digest_size: root_digest_size.into(),
|
||||
hash_tree_size: hash_tree_size.into(),
|
||||
};
|
||||
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
writer
|
||||
.write_all(&self.salt)
|
||||
.map_err(|e| Error::DataWrite("salt", e))?;
|
||||
writer
|
||||
.write_all(&self.root_digest)
|
||||
.map_err(|e| Error::DataWrite("root_digest", e))?;
|
||||
writer
|
||||
.write_all(&self.hash_tree)
|
||||
.map_err(|e| Error::DataWrite("hash_tree", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -646,7 +689,7 @@ mod tests {
|
||||
);
|
||||
assert_matches!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16385]),
|
||||
Err(Error::FieldOutOfBounds(_))
|
||||
Err(Error::IntOutOfBounds(_, _))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod avb;
|
||||
pub mod bootimage;
|
||||
@@ -9,7 +7,10 @@ pub mod compression;
|
||||
pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod lp;
|
||||
pub mod ota;
|
||||
pub mod padding;
|
||||
pub mod payload;
|
||||
pub mod sparse;
|
||||
pub mod verityrs;
|
||||
pub mod zip;
|
||||
|
||||
+739
-261
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
@@ -47,3 +45,35 @@ pub fn write_zeros(mut writer: impl Write + Seek, page_size: u64) -> io::Result<
|
||||
|
||||
Ok(padding)
|
||||
}
|
||||
|
||||
pub trait ZeroPadding {
|
||||
/// Trim trailing zeros. Intermediate zeros before the last non-zero byte
|
||||
/// are kept.
|
||||
fn trim_end_padding(&self) -> &[u8];
|
||||
|
||||
/// Return the slice as an array padded with zeros at the end.
|
||||
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]>;
|
||||
}
|
||||
|
||||
impl ZeroPadding for [u8] {
|
||||
fn trim_end_padding(&self) -> &[u8] {
|
||||
let first_ending_zero = self
|
||||
.iter()
|
||||
.rposition(|b| *b != 0)
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
&self[..first_ending_zero]
|
||||
}
|
||||
|
||||
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]> {
|
||||
if self.len() > N {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = [0u8; N];
|
||||
result[..self.len()].copy_from_slice(self);
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
+675
-182
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
// The gf256 library uses compile-time proc macro code generation. Since
|
||||
// dm-verity supports RS(255, 231) through RS(255, 253), we'll generate RS
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Seek, SeekFrom, Write};
|
||||
|
||||
use zip::{
|
||||
ZipWriter,
|
||||
result::ZipResult,
|
||||
write::{FileOptionExtension, FileOptions, StreamWriter},
|
||||
};
|
||||
|
||||
/// A wrapper around a seekable writer. `W` must implement [`Seek`], but only
|
||||
/// during the creation of a new instance. The resulting type can be stored in a
|
||||
/// parent container where the generic type does not implement [`Seek`].
|
||||
pub struct SeekWriter<W: Write> {
|
||||
inner: W,
|
||||
seek_fn: fn(&mut W, SeekFrom) -> io::Result<u64>,
|
||||
}
|
||||
|
||||
impl<W: Write> SeekWriter<W> {
|
||||
pub fn into_inner(self) -> W {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> SeekWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
seek_fn: W::seek,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SeekWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.inner.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Seek for SeekWriter<W> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
(self.seek_fn)(&mut self.inner, pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an ugly hack to have a single type represent both seekable and
|
||||
/// streaming [`ZipWriter`]s. `W` only needs to implement [`Seek`] when creating
|
||||
/// a seekable instance via [`Self::new_seekable`].
|
||||
pub enum ZipWriterWrapper<W: Write> {
|
||||
Streaming(ZipWriter<StreamWriter<W>>),
|
||||
Seekable(ZipWriter<SeekWriter<W>>),
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> ZipWriterWrapper<W> {
|
||||
pub fn new_seekable(inner: W) -> Self {
|
||||
Self::Seekable(ZipWriter::new(SeekWriter::new(inner)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ZipWriterWrapper<W> {
|
||||
pub fn new_streaming(inner: W) -> Self {
|
||||
Self::Streaming(ZipWriter::new_stream(inner))
|
||||
}
|
||||
|
||||
pub fn start_file(
|
||||
&mut self,
|
||||
name: impl ToString,
|
||||
options: FileOptions<impl FileOptionExtension>,
|
||||
) -> ZipResult<u64> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.start_file(name, options),
|
||||
Self::Seekable(z) => z.start_file(name, options),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> ZipResult<W> {
|
||||
match self {
|
||||
Self::Streaming(z) => Ok(z.finish()?.into_inner()),
|
||||
Self::Seekable(z) => Ok(z.finish()?.into_inner()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for ZipWriterWrapper<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.write(buf),
|
||||
Self::Seekable(z) => z.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.flush(),
|
||||
Self::Seekable(z) => z.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Since avbroot is primarily an application and not a library, the semver
|
||||
//! versioning covers the CLI only. All Rust APIs can change at any time, even
|
||||
|
||||
+11
-10
@@ -1,18 +1,19 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
use std::{
|
||||
process::ExitCode,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn main() {
|
||||
fn main() -> ExitCode {
|
||||
// Set up a cancel signal so we can properly clean up any temporary files.
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
@@ -25,14 +26,14 @@ fn main() {
|
||||
}
|
||||
|
||||
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
|
||||
error!("{e:?}");
|
||||
} else {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Hack to format an integer as an octal string because toml_edit can't output
|
||||
//! octal-formatted integers and many other toml parsers can't parse it either.
|
||||
@@ -12,7 +10,7 @@ use std::{
|
||||
};
|
||||
|
||||
use num_traits::{Num, PrimInt};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use serde::{Deserializer, Serializer, de::Visitor};
|
||||
|
||||
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@@ -30,7 +28,7 @@ where
|
||||
{
|
||||
struct OctalStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
|
||||
impl<T> Visitor<'_> for OctalStrVisitor<T>
|
||||
where
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
|
||||
+411
-161
@@ -1,17 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Write,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Cursor, Read, Seek},
|
||||
io::{self, BufRead, BufReader, Cursor, Read},
|
||||
num::ParseIntError,
|
||||
ops::Range,
|
||||
ops::{Range, RangeFrom},
|
||||
path::{Path, PathBuf},
|
||||
slice,
|
||||
slice, str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -23,14 +22,14 @@ use liblzma::{
|
||||
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPrivateKey;
|
||||
use rsa::RsaPublicKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, warn, Span};
|
||||
use tracing::{Span, debug, debug_span, trace, warn};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{result::ZipError, ZipArchive};
|
||||
use zip::{ZipArchive, result::ZipError};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
crypto::{self, RsaSigningKey},
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer, Header},
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
@@ -43,8 +42,6 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Boot image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash descriptor found in vbmeta header")]
|
||||
@@ -55,37 +52,80 @@ pub enum Error {
|
||||
ParseMagiskVersion(String, #[source] ParseIntError),
|
||||
#[error("Failed to determine Magisk version from: {0:?}")]
|
||||
FindMagiskVersion(PathBuf),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("Boot image error")]
|
||||
BootImage(#[from] bootimage::Error),
|
||||
#[error("Compression error")]
|
||||
Compression(#[from] compression::Error),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("CPIO error")]
|
||||
Cpio(#[from] cpio::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("File I/O error")]
|
||||
File(PathBuf, #[source] io::Error),
|
||||
#[error("Failed to load potentially compressed ramdisk")]
|
||||
RamdiskLoadCompression(#[source] compression::Error),
|
||||
#[error("Failed to save potentially compressed ramdisk")]
|
||||
RamdiskSaveCompression(#[source] compression::Error),
|
||||
#[error("Failed to finalize compressed ramdisk")]
|
||||
RamdiskSaveCompressionFinalize(#[source] io::Error),
|
||||
#[error("Failed to load ramdisk cpio entries")]
|
||||
RamdiskLoadCpio(#[source] cpio::Error),
|
||||
#[error("Failed to save ramdisk cpio entries")]
|
||||
RamdiskSaveCpio(#[source] cpio::Error),
|
||||
#[error("Failed to load potentially compressed kernel")]
|
||||
KernelLoadCompression(#[source] compression::Error),
|
||||
#[error("Failed to read kernel image")]
|
||||
KernelRead(#[source] io::Error),
|
||||
#[error("Failed to load boot image")]
|
||||
BootImageLoad(#[source] bootimage::Error),
|
||||
#[error("Failed to save boot image")]
|
||||
BootImageSave(#[source] bootimage::Error),
|
||||
#[error("Failed to seek boot image")]
|
||||
BootImageSeek(#[source] io::Error),
|
||||
#[error("Failed to encode public key in AVB binary format")]
|
||||
AvbEncodeKey(#[source] avb::Error),
|
||||
#[error("Failed to load AVB header from boot image")]
|
||||
AvbLoad(#[source] avb::Error),
|
||||
#[error("Failed to update AVB header for boot image")]
|
||||
AvbUpdate(#[source] avb::Error),
|
||||
#[error("Failed to load OTA certificate")]
|
||||
OtaCertLoad(#[source] crypto::Error),
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
#[error("Failed to XZ compress entry: {:?}", .0.as_bstr())]
|
||||
XzCompress(Vec<u8>, #[source] io::Error),
|
||||
#[error("Failed to open zip file: {0:?}")]
|
||||
ZipOpen(PathBuf, #[source] ZipError),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to open zip entry #{0}")]
|
||||
ZipIndexOpen(usize, #[source] ZipError),
|
||||
#[error("Failed to open file: {0:?}")]
|
||||
FileOpen(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TargetsError {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Targets validation error: {0}")]
|
||||
TargetValidation(String),
|
||||
#[error("Failed to open boot image: {0}")]
|
||||
Open(String, #[source] io::Error),
|
||||
#[error("Failed to load boot image: {0}")]
|
||||
Load(String, #[source] Error),
|
||||
#[error("Failed to save boot image: {0}")]
|
||||
Save(String, #[source] Error),
|
||||
#[error("Failed to patch boot image: {0}")]
|
||||
Patch(String, #[source] Error),
|
||||
}
|
||||
|
||||
type TargetsResult<T> = std::result::Result<T, TargetsError>;
|
||||
|
||||
fn load_ramdisk(
|
||||
data: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<CpioEntry>, CompressedFormat)> {
|
||||
let raw_reader = Cursor::new(data);
|
||||
let mut reader = CompressedReader::new(raw_reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal)?;
|
||||
let mut reader =
|
||||
CompressedReader::new(raw_reader, false).map_err(Error::RamdiskLoadCompression)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal).map_err(Error::RamdiskLoadCpio)?;
|
||||
|
||||
trace!(
|
||||
"Loaded {:?} ramdisk with {} entries",
|
||||
@@ -102,12 +142,15 @@ fn save_ramdisk(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let mut writer = CompressedWriter::new(raw_writer, format)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal)?;
|
||||
let mut writer =
|
||||
CompressedWriter::new(raw_writer, format).map_err(Error::RamdiskSaveCompression)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal).map_err(Error::RamdiskSaveCpio)?;
|
||||
|
||||
trace!("Wrote {format:?} ramdisk with {} entries", entries.len());
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = writer
|
||||
.finish()
|
||||
.map_err(Error::RamdiskSaveCompressionFinalize)?;
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
@@ -128,7 +171,7 @@ pub trait BootImagePatch {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>>;
|
||||
) -> TargetsResult<Vec<&'a str>>;
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
|
||||
}
|
||||
@@ -149,13 +192,21 @@ 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..27100];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
// - Versions newer than the latest supported version are assumed to support
|
||||
// the same features as the latest version
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30300];
|
||||
const VER_PREINIT_DEVICE: RangeFrom<u32> = 25211..;
|
||||
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 VER_XZ_BACKUP: RangeFrom<u32> = 26403..;
|
||||
|
||||
const ZIP_INIT_LD: &'static str = "lib/arm64-v8a/libinit-ld.so";
|
||||
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,
|
||||
@@ -206,16 +257,21 @@ 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 reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(path.to_owned(), e))?;
|
||||
let mut zip = ZipArchive::new(reader).map_err(|e| Error::ZipOpen(path.to_owned(), e))?;
|
||||
let entry = zip
|
||||
.by_name(Self::ZIP_UTIL_FUNCTIONS)
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
let mut entry = BufReader::new(entry);
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = entry.read_line(&mut line)?;
|
||||
let n = entry
|
||||
.read_line(&mut line)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
if n == 0 {
|
||||
return Err(Error::FindMagiskVersion(path.to_owned()));
|
||||
}
|
||||
@@ -231,14 +287,14 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn xz_compress(reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32)?;
|
||||
fn xz_compress(name: &[u8], reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32).map_err(Error::XzInit)?;
|
||||
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()?;
|
||||
let raw_writer = stream::copy(reader, &mut writer, cancel_signal)
|
||||
.and_then(|_| writer.finish())
|
||||
.map_err(|e| Error::XzCompress(name.to_owned(), e))?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
@@ -327,7 +383,7 @@ impl MagiskRootPatcher {
|
||||
new_path.extend(b".xz");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
|
||||
new_data = Some(CpioEntryData::Data(buf));
|
||||
}
|
||||
}
|
||||
@@ -369,22 +425,24 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
if boot_images.contains_key("init_boot") {
|
||||
targets.push("init_boot");
|
||||
} else if boot_images.contains_key("boot") {
|
||||
targets.push("boot");
|
||||
};
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let zip_reader =
|
||||
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
|
||||
let zip_reader = File::open(&self.apk_path)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(self.apk_path.clone(), e))?;
|
||||
let mut zip =
|
||||
ZipArchive::new(zip_reader).map_err(|e| Error::ZipOpen(self.apk_path.clone(), e))?;
|
||||
|
||||
// Load the first ramdisk. If it doesn't exist, we have to generate one
|
||||
// from scratch.
|
||||
@@ -413,9 +471,13 @@ 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::ZipEntryOpen(Self::ZIP_MAGISKINIT, e))?;
|
||||
let mut data = vec![];
|
||||
zip_entry.read_to_end(&mut data)?;
|
||||
zip_entry
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT, e))?;
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
b"init",
|
||||
@@ -424,29 +486,41 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
));
|
||||
}
|
||||
|
||||
// Add xz-compressed magisk32 and magisk64. We currently unconditionally
|
||||
// include magisk32 because the boot image itself doesn't contain
|
||||
// sufficient information to determine if a device is 64-bit only.
|
||||
let mut xz_files = HashMap::<&str, &[u8]>::new();
|
||||
xz_files.insert(
|
||||
"lib/armeabi-v7a/libmagisk32.so",
|
||||
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") {
|
||||
if zip.file_names().any(|n| n == Self::ZIP_STUB) {
|
||||
debug!("Magisk stub found");
|
||||
xz_files.insert("assets/stub.apk", b"overlay.d/sbin/stub.xz");
|
||||
xz_files.insert(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz");
|
||||
}
|
||||
|
||||
// Add init-ld, which only exists after Magisk commit
|
||||
// 33aebb59763b6ec27209563035303700e998633d
|
||||
if zip.file_names().any(|n| n == Self::ZIP_INIT_LD) {
|
||||
debug!("Magisk init-ld found");
|
||||
xz_files.insert(Self::ZIP_INIT_LD, b"overlay.d/sbin/init-ld.xz");
|
||||
}
|
||||
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip.by_name(source)?;
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
let reader = zip
|
||||
.by_name(source)
|
||||
.map_err(|e| Error::ZipEntryOpen(source, e))?;
|
||||
let buf = Self::xz_compress(source.as_bytes(), reader, cancel_signal)?;
|
||||
|
||||
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
|
||||
}
|
||||
@@ -471,10 +545,9 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
magisk_config.push_str("RECOVERYMODE=false\n");
|
||||
|
||||
if Self::VER_PREINIT_DEVICE.contains(&self.version) {
|
||||
magisk_config.push_str(&format!(
|
||||
"PREINITDEVICE={}\n",
|
||||
self.preinit_device.as_ref().unwrap(),
|
||||
));
|
||||
if let Some(device) = &self.preinit_device {
|
||||
writeln!(&mut magisk_config, "PREINITDEVICE={device}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Magisk normally saves the original SHA1 digest in its config file. It
|
||||
@@ -484,7 +557,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
magisk_config.push_str("SHA1=0000000000000000000000000000000000000000\n");
|
||||
|
||||
if Self::VER_RANDOM_SEED.contains(&self.version) {
|
||||
magisk_config.push_str(&format!("RANDOMSEED={:#x}\n", self.random_seed));
|
||||
writeln!(&mut magisk_config, "RANDOMSEED={:#x}", self.random_seed).unwrap();
|
||||
}
|
||||
|
||||
trace!("Magisk config: {magisk_config:?}");
|
||||
@@ -497,7 +570,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
|
||||
// Repack ramdisk.
|
||||
cpio::sort(&mut entries);
|
||||
cpio::assign_inodes(&mut entries, false)?;
|
||||
cpio::assign_inodes(&mut entries, false).map_err(Error::RamdiskSaveCpio)?;
|
||||
let new_ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
match boot_image {
|
||||
@@ -564,16 +637,22 @@ impl OtaCertPatcher {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data))?;
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(|e| {
|
||||
Error::ZipOpen(str::from_utf8(Self::OTACERTS_PATH).unwrap().into(), e)
|
||||
})?;
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip.by_index(index)?;
|
||||
let zip_entry = zip
|
||||
.by_index(index)
|
||||
.map_err(|e| Error::ZipIndexOpen(index, e))?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {}", zip_entry.name());
|
||||
continue;
|
||||
}
|
||||
|
||||
let certificate = crypto::read_pem_cert(zip_entry)?;
|
||||
let path = PathBuf::from(zip_entry.name());
|
||||
let certificate =
|
||||
crypto::read_pem_cert(&path, zip_entry).map_err(Error::OtaCertLoad)?;
|
||||
certificates.push(certificate);
|
||||
}
|
||||
}
|
||||
@@ -582,7 +661,6 @@ impl OtaCertPatcher {
|
||||
}
|
||||
|
||||
fn patch_ramdisk(
|
||||
&self,
|
||||
ramdisk: &mut Vec<u8>,
|
||||
zip: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -612,10 +690,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
'outer: for (name, info) in boot_images {
|
||||
'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),
|
||||
@@ -627,9 +705,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)
|
||||
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
if entries.iter().any(|e| e.path == Self::OTACERTS_PATH) {
|
||||
targets.push(*name);
|
||||
targets.push(name);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
@@ -645,7 +724,8 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
BootImage::VendorV3Through4(b) => &mut b.ramdisks,
|
||||
};
|
||||
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())?;
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())
|
||||
.map_err(Error::OtaCertZip)?;
|
||||
trace!("Generated new {} byte otacerts.zip", new_zip.len());
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
@@ -653,7 +733,7 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
if Self::patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -667,6 +747,139 @@ 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 binary_key = avb::encode_public_key(&self.key).map_err(Error::AvbEncodeKey)?;
|
||||
let data = CpioEntryData::Data(binary_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,
|
||||
) -> TargetsResult<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)
|
||||
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
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(TargetsError::TargetValidation(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_unstable();
|
||||
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
|
||||
@@ -694,19 +907,22 @@ impl PrepatchedImagePatcher {
|
||||
}
|
||||
|
||||
fn load_prepatched_image(&self) -> Result<BootImage> {
|
||||
let raw_reader =
|
||||
File::open(&self.prepatched).map_err(|e| Error::File(self.prepatched.clone(), e))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))?;
|
||||
let reader = File::open(&self.prepatched)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(self.prepatched.clone(), e))?;
|
||||
|
||||
Ok(boot_image)
|
||||
BootImage::from_reader(reader).map_err(Error::BootImageLoad)
|
||||
}
|
||||
|
||||
fn get_kmi_version(kernel: &[u8]) -> Result<Option<String>> {
|
||||
let mut decompressed = vec![];
|
||||
{
|
||||
let raw_reader = Cursor::new(kernel);
|
||||
let mut reader = CompressedReader::new(raw_reader, true)?;
|
||||
reader.read_to_end(&mut decompressed)?;
|
||||
let mut reader =
|
||||
CompressedReader::new(raw_reader, true).map_err(Error::KernelLoadCompression)?;
|
||||
reader
|
||||
.read_to_end(&mut decompressed)
|
||||
.map_err(Error::KernelRead)?;
|
||||
}
|
||||
|
||||
let regex = Regex::new(Self::VERSION_REGEX).unwrap();
|
||||
@@ -738,8 +954,10 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
let prepatched_image = self.load_prepatched_image()?;
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let prepatched_image = self
|
||||
.load_prepatched_image()
|
||||
.map_err(|e| TargetsError::Load("prepatched".to_owned(), e))?;
|
||||
|
||||
let has_kernel = match prepatched_image {
|
||||
BootImage::V0Through2(b) => !b.kernel.is_empty(),
|
||||
@@ -753,7 +971,7 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
targets.push("init_boot");
|
||||
} else if boot_images.contains_key("boot") {
|
||||
targets.push("boot");
|
||||
};
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
@@ -926,36 +1144,90 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_boot_image(reader: &mut dyn ReadSeek) -> Result<BootImageInfo> {
|
||||
let (header, footer, image_size) = avb::load_image(&mut *reader).map_err(Error::AvbLoad)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
|
||||
let section_reader =
|
||||
SectionReader::new(reader, 0, footer.original_image_size).map_err(Error::BootImageSeek)?;
|
||||
let boot_image = BootImage::from_reader(section_reader).map_err(Error::BootImageLoad)?;
|
||||
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
trace!("Loaded {image_size} byte boot image");
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn save_boot_image(
|
||||
writer: &mut dyn WriteSeek,
|
||||
info: &mut BootImageInfo,
|
||||
key: &RsaSigningKey,
|
||||
) -> Result<()> {
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info
|
||||
.header
|
||||
.appended_descriptor_mut()
|
||||
.map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image
|
||||
.to_writer(&mut hashing_writer)
|
||||
.map_err(Error::BootImageSave)?;
|
||||
let (writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position().map_err(Error::BootImageSeek)?;
|
||||
"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)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
info.header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(
|
||||
writer,
|
||||
&info.header,
|
||||
&mut info.footer,
|
||||
Some(info.image_size),
|
||||
)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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>> {
|
||||
) -> TargetsResult<HashMap<&'a str, BootImageInfo>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
names
|
||||
.par_iter()
|
||||
.map(|name| {
|
||||
.map(|&name| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut reader = open_input(name)?;
|
||||
let mut reader =
|
||||
open_input(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let (header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let info =
|
||||
load_boot_image(&mut reader).map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
|
||||
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
|
||||
let boot_image = BootImage::from_reader(section_reader)?;
|
||||
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
trace!("Loaded {image_size} byte boot image: {name}");
|
||||
|
||||
Ok((*name, info))
|
||||
Ok((name, info))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -969,12 +1241,17 @@ pub fn patch_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<HashSet<&'a str>> {
|
||||
) -> TargetsResult<HashSet<&'a str>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
if patchers.is_empty() {
|
||||
debug!("Skip loading boot images; nothing to patch");
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
// Preparse all images. Some patchers need to inspect every candidate.
|
||||
let mut images = load_boot_images(names, open_input)?;
|
||||
|
||||
@@ -986,14 +1263,14 @@ pub fn patch_boot_images<'a>(
|
||||
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()))
|
||||
Err(TargetsError::NoTargets(p.patcher_name()))
|
||||
} else {
|
||||
debug!("Found patcher targets: {targets:?}");
|
||||
Ok(targets)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<TargetsResult<Vec<_>>>()?;
|
||||
|
||||
debug!("All patcher targets: {all_targets:?}");
|
||||
|
||||
@@ -1015,49 +1292,22 @@ pub fn patch_boot_images<'a>(
|
||||
// Apply all patches.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.try_for_each(|(_, (info, patchers))| -> Result<()> {
|
||||
.try_for_each(|(&name, (info, patchers))| -> TargetsResult<()> {
|
||||
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)
|
||||
.map_err(|e| TargetsError::Patch(name.to_owned(), e))
|
||||
})
|
||||
})?;
|
||||
|
||||
// Resign and write new images.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.map(|(name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
groups.par_iter_mut().try_for_each(|(&name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut writer = open_output(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info.header.appended_descriptor_mut()?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
save_boot_image(&mut writer, info, key).map_err(|e| TargetsError::Save(name.to_owned(), e))
|
||||
})?;
|
||||
|
||||
let writer = open_output(name)?;
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
descriptor.hash_algorithm = "sha256".to_owned();
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if !info.header.public_key.is_empty() {
|
||||
debug!("Signing boot image");
|
||||
info.header.set_algo_for_key(key)?;
|
||||
info.header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(writer, &info.header, &mut info.footer, info.image_size)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(groups.keys().cloned().collect())
|
||||
Ok(groups.keys().copied().collect())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod boot;
|
||||
pub mod otacert;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use x509_cert::{der::asn1::BitString, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
|
||||
use x509_cert::{Certificate, der::asn1::BitString};
|
||||
use zip::{CompressionMethod, DateTime, ZipWriter, result::ZipError, write::SimpleFileOptions};
|
||||
|
||||
use crate::{crypto, format::ota};
|
||||
|
||||
@@ -19,12 +17,10 @@ pub enum Error {
|
||||
ZipTooSmall(usize),
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("Failed to write otacerts zip")]
|
||||
ZipWrite(#[source] ZipError),
|
||||
#[error("Failed to write certificate to otacerts zip")]
|
||||
CertWrite(#[source] crypto::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -83,8 +79,11 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
CompressionMethod::Stored
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(compression_method);
|
||||
let name = "ota.x509.pem";
|
||||
writer.start_file(name, options).map_err(Error::ZipWrite)?;
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
@@ -92,7 +91,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
let mut modified = cert.clone();
|
||||
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
|
||||
modified.signature = BitString::from_bytes(&[])?;
|
||||
// An empty ASN.1 bit string is always valid.
|
||||
modified.signature =
|
||||
BitString::from_bytes(&[]).expect("Empty ASN.1 bit string was invalid");
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
|
||||
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
|
||||
@@ -111,9 +112,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &cert)?;
|
||||
crypto::write_pem_cert(Path::new(name), &mut writer, &cert).map_err(Error::CertWrite)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = writer.finish().map_err(Error::ZipWrite)?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
+56
-55
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, SeekFrom},
|
||||
@@ -11,19 +9,19 @@ use std::{
|
||||
|
||||
use memchr::memmem;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use rsa::RsaPrivateKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, Span};
|
||||
use tracing::{Span, debug, debug_span, trace};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer},
|
||||
ota,
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
stream::{self, ReadFixedSizeExt, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -35,14 +33,16 @@ pub enum Error {
|
||||
NoFooter,
|
||||
#[error("No hash tree descriptor found in vbmeta header")]
|
||||
NoHashTreeDescriptor,
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to update AVB header")]
|
||||
AvbUpdate(#[source] avb::Error),
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to read image data")]
|
||||
ReadData(#[source] io::Error),
|
||||
#[error("Failed to write image data")]
|
||||
WriteData(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -111,7 +111,7 @@ pub fn patch_system_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
certificate: &Certificate,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
|
||||
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
|
||||
@@ -121,31 +121,38 @@ pub fn patch_system_image(
|
||||
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
|
||||
let (mut header, footer, image_size) =
|
||||
avb::load_image(input.reopen_boxed().map_err(Error::ReadData)?)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
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()
|
||||
.map(|chunk| -> Result<Vec<Range<u64>>> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::ReadData)?;
|
||||
|
||||
let offset = chunk * CHUNK_SIZE;
|
||||
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
reader.read_exact(&mut buf)?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ReadData)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(Error::ReadData)?;
|
||||
let buf = reader
|
||||
.read_vec_exact(size as usize)
|
||||
.map_err(Error::ReadData)?;
|
||||
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
let mut ranges = Vec::<Range<u64>>::new();
|
||||
|
||||
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
|
||||
@@ -157,14 +164,17 @@ pub fn patch_system_image(
|
||||
};
|
||||
|
||||
let zip_size = bounds_rel.end - bounds_rel.start;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)
|
||||
.map_err(Error::OtaCertZip)?;
|
||||
|
||||
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(bounds.start))?;
|
||||
writer.write_all(&new_zip)?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(bounds.start))
|
||||
.map_err(Error::WriteData)?;
|
||||
writer.write_all(&new_zip).map_err(Error::WriteData)?;
|
||||
|
||||
ranges.push(bounds);
|
||||
}
|
||||
@@ -180,36 +190,27 @@ pub fn patch_system_image(
|
||||
return Err(Error::OldZipNotFound);
|
||||
}
|
||||
|
||||
let update_ranges = if descriptor.hash_algorithm == "sha1" {
|
||||
// Promote to a secure algorithm. SHA1 is allowed for verification only.
|
||||
// The entire hash tree and FEC data will need to be recomputed.
|
||||
let new_algorithm = "sha256".to_owned();
|
||||
// Only need to update the hash tree and FEC data corresponding to the
|
||||
// modified regions.
|
||||
let update_ranges = Some(modified_ranges.as_slice());
|
||||
|
||||
debug!(
|
||||
"Changing insecure hash algorithm {} to {new_algorithm}",
|
||||
descriptor.hash_algorithm,
|
||||
);
|
||||
|
||||
descriptor.hash_algorithm = new_algorithm;
|
||||
None
|
||||
} else {
|
||||
// Only need to update the hash tree and FEC data corresponding to the
|
||||
// modified regions.
|
||||
Some(modified_ranges.as_slice())
|
||||
};
|
||||
|
||||
descriptor.update(input, output, update_ranges, cancel_signal)?;
|
||||
descriptor
|
||||
.update(input, output, update_ranges, cancel_signal)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
debug!("Signing system image");
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
header.set_algo_for_key(key).map_err(Error::AvbUpdate)?;
|
||||
header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed()?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
|
||||
let writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
@@ -217,15 +218,15 @@ pub fn patch_system_image(
|
||||
let hash_tree_end = descriptor
|
||||
.tree_offset
|
||||
.checked_add(descriptor.tree_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
|
||||
.ok_or(Error::IntOverflow("hash_tree_end"))?;
|
||||
let fec_data_end = descriptor
|
||||
.fec_offset
|
||||
.checked_add(descriptor.fec_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
|
||||
.ok_or(Error::IntOverflow("fec_data_end"))?;
|
||||
let header_end = footer
|
||||
.vbmeta_offset
|
||||
.checked_add(footer.vbmeta_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
|
||||
.ok_or(Error::IntOverflow("avb_end"))?;
|
||||
let footer_start = image_size - Footer::SIZE as u64;
|
||||
|
||||
let other_ranges = util::merge_overlapping(&[
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#![allow(clippy::all)]
|
||||
#![allow(clippy::nursery)]
|
||||
#![allow(clippy::pedantic)]
|
||||
|
||||
pub mod build {
|
||||
pub mod tools {
|
||||
pub mod releasetools {
|
||||
|
||||
+28
-150
@@ -1,18 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
@@ -124,69 +121,26 @@ impl<W: Write> WriteZerosExt for W {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for readers to read strings.
|
||||
pub trait ReadStringExt {
|
||||
/// Read exact sized string.
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String>;
|
||||
/// Extensions for readers to read fixed-size buffers.
|
||||
pub trait ReadFixedSizeExt {
|
||||
/// Read fixed-size array.
|
||||
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]>;
|
||||
|
||||
/// Read string with maximum size and trim trailing zeros.
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String>;
|
||||
/// Read fixed-sized [`Vec`].
|
||||
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
impl<R: Read> ReadStringExt for R {
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String> {
|
||||
impl<R: Read> ReadFixedSizeExt for R {
|
||||
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]> {
|
||||
let mut buf = [0u8; N];
|
||||
self.read_exact(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String> {
|
||||
let mut buf = vec![0u8; max_size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
let after_last_non_zero = buf
|
||||
.iter()
|
||||
.rev()
|
||||
.position(|&b| b != 0)
|
||||
.map_or(0, |i| buf.len() - i);
|
||||
buf.resize(after_last_non_zero, 0);
|
||||
buf.shrink_to_fit();
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for writers to write strings.
|
||||
pub trait WriteStringExt {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl<W: Write> WriteStringExt for W {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()> {
|
||||
if data.len() > max_size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{data:?} exceeds maximum size of {max_size} bytes"),
|
||||
));
|
||||
}
|
||||
|
||||
self.write_all(data.as_bytes())?;
|
||||
|
||||
let num_zeros = (max_size - data.len()) as u64;
|
||||
self.write_zeros_exact(num_zeros)?;
|
||||
|
||||
Ok(())
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,19 +153,19 @@ pub trait Reopen: Sized {
|
||||
|
||||
impl<R: Read + Reopen> Reopen for BufReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufReader::new(self.get_ref().reopen()?))
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Reopen> Reopen for BufWriter<W> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufWriter::new(self.get_ref().reopen()?))
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// A reader wrapper that implements [`Seek`], but only for reporting the
|
||||
/// current file position.
|
||||
pub struct CountingReader<R: Read> {
|
||||
pub struct CountingReader<R> {
|
||||
inner: R,
|
||||
offset: u64,
|
||||
}
|
||||
@@ -249,7 +203,7 @@ impl<R: Read> Seek for CountingReader<R> {
|
||||
|
||||
/// A writer wrapper that implements [`Seek`], but only for reporting the
|
||||
/// current file position.
|
||||
pub struct CountingWriter<W: Write> {
|
||||
pub struct CountingWriter<W> {
|
||||
inner: W,
|
||||
offset: u64,
|
||||
}
|
||||
@@ -290,7 +244,7 @@ impl<W: Write> Seek for CountingWriter<W> {
|
||||
}
|
||||
|
||||
/// A reader wrapper that hashes data as it's being read.
|
||||
pub struct HashingReader<R: Read> {
|
||||
pub struct HashingReader<R> {
|
||||
inner: R,
|
||||
context: Context,
|
||||
}
|
||||
@@ -314,7 +268,7 @@ impl<R: Read> Read for HashingReader<R> {
|
||||
}
|
||||
|
||||
/// A writer wrapper that hashes data as it's being written.
|
||||
pub struct HashingWriter<W: Write> {
|
||||
pub struct HashingWriter<W> {
|
||||
inner: W,
|
||||
context: Context,
|
||||
}
|
||||
@@ -342,7 +296,7 @@ impl<W: Write> Write for HashingWriter<W> {
|
||||
}
|
||||
|
||||
/// A reader wrapper that only allows reading a specific section of a file.
|
||||
pub struct SectionReader<R: Read + Seek> {
|
||||
pub struct SectionReader<R> {
|
||||
inner: R,
|
||||
start: u64,
|
||||
size: u64,
|
||||
@@ -416,38 +370,6 @@ impl<R: Read + Seek> Seek for SectionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer wrapper that seeks instead of writing when a write buffer consists
|
||||
/// solely of zeros.
|
||||
#[derive(Debug)]
|
||||
pub struct HolePunchingWriter<W: Write + Seek> {
|
||||
inner: W,
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> HolePunchingWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> W {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> Write for HolePunchingWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
if util::is_zero(buf) {
|
||||
self.inner.seek(SeekFrom::Current(buf.len() as i64))?;
|
||||
Ok(buf.len())
|
||||
} else {
|
||||
self.inner.write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file wrapper that uses a userspace file offset. A reopened instance uses
|
||||
/// the same underlying kernel file descriptor, but a new userspace file offset,
|
||||
/// initially set to 0.
|
||||
@@ -577,9 +499,7 @@ pub struct SharedCursor {
|
||||
|
||||
impl SharedCursor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,9 +640,8 @@ mod tests {
|
||||
use ring::digest::Context;
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, HolePunchingWriter,
|
||||
PSeekFile, ReadDiscardExt, ReadStringExt, Reopen, SectionReader, SharedCursor,
|
||||
WriteStringExt, WriteZerosExt,
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
|
||||
Reopen, SectionReader, SharedCursor, WriteZerosExt,
|
||||
};
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
@@ -759,32 +678,6 @@ mod tests {
|
||||
assert_eq!(&writer.into_inner(), b"\0\0foo\0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_string() {
|
||||
let mut reader = Cursor::new(b"foo\0\0bar\0\0");
|
||||
|
||||
assert_eq!(reader.read_string_exact(3).unwrap(), "foo");
|
||||
assert_eq!(reader.read_string_exact(0).unwrap(), "");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(3).unwrap(), "foo");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(10).unwrap(), "foo\0\0bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_string() {
|
||||
let mut writer = Cursor::new([0xffu8; 8]);
|
||||
|
||||
writer.write_string_padded("foobar", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobar\0\0");
|
||||
|
||||
writer.rewind().unwrap();
|
||||
writer.write_string_padded("foobarhi", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobarhi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counting_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
@@ -882,21 +775,6 @@ mod tests {
|
||||
assert_eq!(raw_reader.stream_position().unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hole_punching_writer() {
|
||||
let raw_writer = Cursor::new(b"foobar foobar".to_owned());
|
||||
let mut writer = HolePunchingWriter::new(raw_writer);
|
||||
|
||||
writer.write_all(b"hello").unwrap();
|
||||
writer.write_all(b"").unwrap();
|
||||
writer.write_all(b"\0").unwrap();
|
||||
writer.write_all(b"\0\0").unwrap();
|
||||
writer.write_all(b"world").unwrap();
|
||||
|
||||
let raw_writer = writer.into_inner();
|
||||
assert_eq!(&raw_writer.into_inner(), b"hellor fworld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pseek_file() {
|
||||
let raw_file = tempfile::tempfile().unwrap();
|
||||
|
||||
+354
-30
@@ -1,11 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{self, Display},
|
||||
mem,
|
||||
ops::{
|
||||
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
|
||||
},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use num_traits::PrimInt;
|
||||
use num_traits::{NumCast, PrimInt};
|
||||
use thiserror::Error;
|
||||
|
||||
pub const ZEROS: [u8; 16384] = [0u8; 16384];
|
||||
|
||||
@@ -23,6 +30,263 @@ impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a precomputed [`Debug`] string.
|
||||
#[derive(Clone)]
|
||||
pub struct DebugString(String);
|
||||
|
||||
impl DebugString {
|
||||
pub fn new(value: impl fmt::Debug) -> Self {
|
||||
Self(format!("{value:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DebugString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
pub enum AnyRange<T> {
|
||||
Range(Range<T>),
|
||||
RangeFrom(RangeFrom<T>),
|
||||
RangeFull(RangeFull),
|
||||
RangeInclusive(RangeInclusive<T>),
|
||||
RangeTo(RangeTo<T>),
|
||||
RangeToInclusive(RangeToInclusive<T>),
|
||||
}
|
||||
|
||||
impl<T> AnyRange<T> {
|
||||
pub fn with_bounds(start: Bound<T>, end: Bound<T>) -> Option<Self> {
|
||||
let result = match (start, end) {
|
||||
(Bound::Included(s), Bound::Excluded(e)) => Self::Range(s..e),
|
||||
(Bound::Included(s), Bound::Unbounded) => Self::RangeFrom(s..),
|
||||
(Bound::Unbounded, Bound::Unbounded) => Self::RangeFull(..),
|
||||
(Bound::Included(s), Bound::Included(e)) => Self::RangeInclusive(s..=e),
|
||||
(Bound::Unbounded, Bound::Excluded(e)) => Self::RangeTo(..e),
|
||||
(Bound::Unbounded, Bound::Included(e)) => Self::RangeToInclusive(..=e),
|
||||
(Bound::Excluded(_), _) => return None,
|
||||
};
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialOrd<T>> AnyRange<T> {
|
||||
pub fn contains<U>(&self, item: &U) -> bool
|
||||
where
|
||||
T: PartialOrd<U>,
|
||||
U: ?Sized + PartialOrd<T>,
|
||||
{
|
||||
<Self as RangeBounds<T>>::contains(self, item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for AnyRange<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Range(r) => r.fmt(f),
|
||||
Self::RangeFrom(r) => r.fmt(f),
|
||||
Self::RangeFull(r) => r.fmt(f),
|
||||
Self::RangeInclusive(r) => r.fmt(f),
|
||||
Self::RangeTo(r) => r.fmt(f),
|
||||
Self::RangeToInclusive(r) => r.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeBounds<T> for AnyRange<T> {
|
||||
fn start_bound(&self) -> Bound<&T> {
|
||||
match self {
|
||||
Self::Range(r) => r.start_bound(),
|
||||
Self::RangeFrom(r) => r.start_bound(),
|
||||
Self::RangeFull(r) => r.start_bound(),
|
||||
Self::RangeInclusive(r) => r.start_bound(),
|
||||
Self::RangeTo(r) => r.start_bound(),
|
||||
Self::RangeToInclusive(r) => r.start_bound(),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_bound(&self) -> Bound<&T> {
|
||||
match self {
|
||||
Self::Range(r) => r.end_bound(),
|
||||
Self::RangeFrom(r) => r.end_bound(),
|
||||
Self::RangeFull(r) => r.end_bound(),
|
||||
Self::RangeInclusive(r) => r.end_bound(),
|
||||
Self::RangeTo(r) => r.end_bound(),
|
||||
Self::RangeToInclusive(r) => r.end_bound(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Range<T>> for AnyRange<T> {
|
||||
fn from(value: Range<T>) -> Self {
|
||||
Self::Range(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeFrom<T>> for AnyRange<T> {
|
||||
fn from(value: RangeFrom<T>) -> Self {
|
||||
Self::RangeFrom(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeFull> for AnyRange<T> {
|
||||
fn from(value: RangeFull) -> Self {
|
||||
Self::RangeFull(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeInclusive<T>> for AnyRange<T> {
|
||||
fn from(value: RangeInclusive<T>) -> Self {
|
||||
Self::RangeInclusive(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeTo<T>> for AnyRange<T> {
|
||||
fn from(value: RangeTo<T>) -> Self {
|
||||
Self::RangeTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeToInclusive<T>> for AnyRange<T> {
|
||||
fn from(value: RangeToInclusive<T>) -> Self {
|
||||
Self::RangeToInclusive(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic type that can represent any 64-bit or smaller primitive
|
||||
/// integer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LargeInt {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
}
|
||||
|
||||
impl fmt::Display for LargeInt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Signed(n) => n.fmt(f),
|
||||
Self::Unsigned(n) => n.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic type that can represent any 64-bit or smaller primitive
|
||||
/// integer range.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LargeIntRange {
|
||||
Signed(AnyRange<i64>),
|
||||
Unsigned(AnyRange<u64>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for LargeIntRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Signed(r) => r.fmt(f),
|
||||
Self::Unsigned(r) => r.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned when a value is not within a specific range.
|
||||
#[derive(Clone, Debug, Error)]
|
||||
#[error("Integer value {value} not in bounds: {range:?}")]
|
||||
pub struct OutOfBoundsError {
|
||||
value: LargeInt,
|
||||
range: LargeIntRange,
|
||||
}
|
||||
|
||||
/// Verify that `value` is within `bounds` and then return `value` if it is.
|
||||
pub fn check_bounds<T: PrimInt>(
|
||||
value: T,
|
||||
range: impl Into<AnyRange<T>>,
|
||||
) -> Result<T, OutOfBoundsError> {
|
||||
const {
|
||||
assert!(
|
||||
mem::size_of::<T>() <= 8,
|
||||
"Integer must be 64 bits or smaller"
|
||||
);
|
||||
}
|
||||
|
||||
let range = range.into();
|
||||
|
||||
if !range.contains(&value) {
|
||||
let value = if T::min_value() != T::zero() {
|
||||
LargeInt::Signed(NumCast::from(value).unwrap())
|
||||
} else {
|
||||
LargeInt::Unsigned(NumCast::from(value).unwrap())
|
||||
};
|
||||
|
||||
let range = if T::min_value() != T::zero() {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
LargeIntRange::Signed(AnyRange::with_bounds(start, end).unwrap())
|
||||
} else {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
LargeIntRange::Unsigned(AnyRange::with_bounds(start, end).unwrap())
|
||||
};
|
||||
|
||||
return Err(OutOfBoundsError { value, range });
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Try to cast `value` to primitive integer type `T`. If it does not fit, the
|
||||
/// error will indicate the valid range of values.
|
||||
pub fn try_cast<T: PrimInt, V: PrimInt>(value: V) -> Result<T, OutOfBoundsError> {
|
||||
const {
|
||||
assert!(
|
||||
mem::size_of::<T>() <= 8,
|
||||
"Integer must be 64 bits or smaller"
|
||||
);
|
||||
}
|
||||
|
||||
NumCast::from(value).ok_or_else(|| {
|
||||
let value = if V::min_value() != V::zero() {
|
||||
LargeInt::Signed(NumCast::from(value).unwrap())
|
||||
} else {
|
||||
LargeInt::Unsigned(NumCast::from(value).unwrap())
|
||||
};
|
||||
|
||||
let range = if T::min_value() != T::zero() {
|
||||
let min = NumCast::from(T::min_value()).unwrap();
|
||||
let max = NumCast::from(T::max_value()).unwrap();
|
||||
|
||||
LargeIntRange::Signed((min..=max).into())
|
||||
} else {
|
||||
let min = NumCast::from(T::min_value()).unwrap();
|
||||
let max = NumCast::from(T::max_value()).unwrap();
|
||||
|
||||
LargeIntRange::Unsigned((min..=max).into())
|
||||
};
|
||||
|
||||
OutOfBoundsError { value, range }
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a byte slice is all zeros.
|
||||
pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
while !buf.is_empty() {
|
||||
@@ -49,16 +313,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
|
||||
@@ -125,29 +379,99 @@ where
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn join(into_iter: impl IntoIterator<Item = impl Display>, sep: &str) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
for (i, item) in into_iter.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push_str(sep);
|
||||
}
|
||||
|
||||
write!(result, "{item}").expect("Failed to allocate");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn sort<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
|
||||
let mut items = iter.collect::<Vec<_>>();
|
||||
items.sort();
|
||||
items
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_any_range() {
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Excluded(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Unbounded).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..));
|
||||
|
||||
let range = AnyRange::<i32>::with_bounds(Bound::Unbounded, Bound::Unbounded).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Included(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..=1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Excluded(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Included(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..=1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_bounds() {
|
||||
check_bounds(i64::MIN, ..).unwrap();
|
||||
check_bounds(i64::MAX, ..).unwrap();
|
||||
check_bounds(u64::MIN, ..).unwrap();
|
||||
check_bounds(u64::MAX, ..).unwrap();
|
||||
check_bounds(0, -1..=1).unwrap();
|
||||
|
||||
let err = check_bounds(i8::MAX, 0..=0).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Signed(127));
|
||||
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(0..=0)));
|
||||
|
||||
let err = check_bounds(u8::MAX, 0..=0).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Unsigned(255));
|
||||
assert_eq!(err.range, LargeIntRange::Unsigned(AnyRange::from(0..=0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_cast() {
|
||||
let value: u8 = try_cast(255u16).unwrap();
|
||||
assert_eq!(value, 255);
|
||||
|
||||
let err = try_cast::<i8, _>(256u16).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Unsigned(256));
|
||||
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(-128..=127)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_overlaps() {
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
|
||||
assert!(!ranges_overlaps(&[0..4], &(0..0)));
|
||||
assert!(ranges_overlaps(&[0..4], &(0..4)));
|
||||
assert!(ranges_overlaps(&[0..4], &(1..4)));
|
||||
assert!(ranges_overlaps(&[0..4], &(0..3)));
|
||||
assert!(!ranges_overlaps(&[0..4], &(4..5)));
|
||||
assert!(ranges_overlaps(&[5..8], &(5..9)));
|
||||
assert!(ranges_overlaps(&[5..8], &(4..8)));
|
||||
assert!(ranges_overlaps(&[5..8], &(4..9)));
|
||||
assert!(ranges_overlaps(&[0..4, 5..8], &(4..5)));
|
||||
assert!(ranges_overlaps(&[0..4, 5..8], &(0..9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_contains() {
|
||||
assert_eq!(ranges_contains(&[0..4], &0), true);
|
||||
assert_eq!(ranges_contains(&[0..4], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
|
||||
assert!(ranges_contains(&[0..4], &0));
|
||||
assert!(!ranges_contains(&[0..4], &4));
|
||||
assert!(!ranges_contains(&[0..4, 5..8], &4));
|
||||
assert!(ranges_contains(&[0..4, 5..8], &6));
|
||||
}
|
||||
}
|
||||
|
||||
+398
-113
@@ -1,10 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// 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,11 +12,16 @@ use rsa::RsaPrivateKey;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::avb::{self, AppendedDescriptorMut, AppendedDescriptorRef},
|
||||
stream::{self, SharedCursor},
|
||||
crypto::RsaSigningKey,
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
|
||||
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
|
||||
KernelCmdlineDescriptor, PropertyDescriptor,
|
||||
},
|
||||
stream::SharedCursor,
|
||||
};
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
fn get_test_key() -> RsaSigningKey {
|
||||
let data = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
|
||||
@@ -28,157 +31,439 @@ fn get_test_key() -> RsaPrivateKey {
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
|
||||
));
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
|
||||
RsaSigningKey::Internal(key)
|
||||
}
|
||||
|
||||
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", 47),
|
||||
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(),
|
||||
[
|
||||
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
|
||||
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
|
||||
0xae, 0x0a, 0x0f, 0x0a, 0x4a, 0x15, 0x91, 0x0e, 0x4d, 0x18, 0x31, 0x48, 0x20, 0xe8,
|
||||
0x44, 0x62, 0x07, 0x98, 0x43, 0x30, 0xee, 0x2d, 0x20, 0x28, 0xc3, 0x94, 0xc6, 0x0e,
|
||||
0x86, 0xa3, 0xa7, 0x17, 0x36, 0xfd, 0x50, 0x7c,
|
||||
],
|
||||
);
|
||||
|
||||
// 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", 47),
|
||||
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(),
|
||||
[
|
||||
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
|
||||
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
|
||||
0xe1, 0xc5, 0xac, 0x4f, 0x98, 0xb3, 0x0a, 0xed, 0xfc, 0xee, 0xa2, 0x6a, 0x77, 0xf4,
|
||||
0xe5, 0x69, 0xa0, 0xcd, 0x7a, 0xd1, 0xfe, 0x1d, 0x07, 0xd1, 0x25, 0xc6, 0x22, 0xe0,
|
||||
0x25, 0xcb, 0xe9, 0x75, 0x50, 0xe4, 0xae, 0x59,
|
||||
],
|
||||
);
|
||||
|
||||
// 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", 47),
|
||||
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(),
|
||||
[
|
||||
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
|
||||
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
|
||||
0xaf, 0x4c, 0x56, 0x64, 0x3a, 0xd1, 0x06, 0x01, 0xda, 0x2e, 0x05, 0x67, 0xd1, 0x01,
|
||||
0xe3, 0xcb, 0x7b, 0x1e, 0xeb, 0x05, 0x89, 0xeb, 0x80, 0xcc, 0x17, 0x0c, 0x24, 0x73,
|
||||
0x0d, 0xcb, 0x36, 0xfa, 0x17, 0xbd, 0x20, 0x7e,
|
||||
],
|
||||
);
|
||||
|
||||
// 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", 47),
|
||||
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(),
|
||||
[
|
||||
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
|
||||
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
|
||||
0xee, 0xbf, 0x95, 0x31, 0xff, 0xc7, 0xf5, 0xb9, 0x4f, 0x18, 0x57, 0x46, 0x37, 0xbb,
|
||||
0xce, 0x7b, 0xa7, 0x26, 0x18, 0x5a, 0x3c, 0x41, 0xb2, 0x2e, 0xb7, 0x86, 0x51, 0xdc,
|
||||
0xf6, 0x26, 0x86, 0xf3, 0xc7, 0x96, 0x23, 0xed,
|
||||
],
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
+289
-65
@@ -1,19 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::bootimage::{BootImage, BootImageExt},
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
|
||||
bootimage::{
|
||||
self, BootImage, BootImageExt, BootImageV0Through2, BootImageV3Through4, RamdiskMeta,
|
||||
V1Extra, V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
|
||||
},
|
||||
},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
use pkcs8::DecodePrivateKey;
|
||||
use rsa::RsaPrivateKey;
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
fn get_test_key() -> RsaSigningKey {
|
||||
let data = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
|
||||
@@ -23,103 +28,322 @@ fn get_test_key() -> RsaPrivateKey {
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
|
||||
));
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
|
||||
RsaSigningKey::Internal(key)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{Cursor, Read, Seek, Write};
|
||||
|
||||
|
||||
+51
-11
@@ -1,28 +1,68 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
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 +80,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.
@@ -0,0 +1,426 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{io::Cursor, num::NonZeroU64};
|
||||
|
||||
use avbroot::{
|
||||
format::lp::{
|
||||
BlockDevice, BlockDeviceFlags, Extent, ExtentType, HeaderFlags, ImageType, Metadata,
|
||||
MetadataSlot, Partition, PartitionAttributes, PartitionGroup, PartitionGroupFlags,
|
||||
},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
|
||||
fn round_trip(metadata: &Metadata, sha512: &[u8; 64]) {
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
metadata.to_writer(&mut writer).unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
let mut reader = Cursor::new(&data);
|
||||
let new_metadata = Metadata::from_reader(&mut reader).unwrap();
|
||||
|
||||
assert_eq!(&new_metadata, metadata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_empty_image() {
|
||||
// Layout from Google Pixel 9 Pro XL stock factory image:
|
||||
// komodo-ad1a.240530.047-factory-bb04e484.zip -> super_empty.img
|
||||
let metadata = Metadata {
|
||||
image_type: ImageType::Empty,
|
||||
metadata_max_size: 65536,
|
||||
metadata_slot_count: 3,
|
||||
logical_block_size: 4096,
|
||||
slots: vec![MetadataSlot {
|
||||
major_version: 10,
|
||||
minor_version: 2,
|
||||
groups: vec![
|
||||
PartitionGroup {
|
||||
name: "default".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: None,
|
||||
partitions: vec![],
|
||||
},
|
||||
PartitionGroup {
|
||||
name: "google_dynamic_partitions_a".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: NonZeroU64::new(8527020032),
|
||||
partitions: vec![
|
||||
Partition {
|
||||
name: "system_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "system_dlkm_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "system_ext_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "product_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_dlkm_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
],
|
||||
},
|
||||
PartitionGroup {
|
||||
name: "google_dynamic_partitions_b".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: NonZeroU64::new(8527020032),
|
||||
partitions: vec![
|
||||
Partition {
|
||||
name: "system_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "system_dlkm_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "system_ext_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "product_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_dlkm_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
block_devices: vec![BlockDevice {
|
||||
first_logical_sector: 2048,
|
||||
alignment: 1048576,
|
||||
alignment_offset: 0,
|
||||
size: 8531214336,
|
||||
partition_name: "super".into(),
|
||||
flags: BlockDeviceFlags::empty(),
|
||||
}],
|
||||
flags: HeaderFlags::VIRTUAL_AB_DEVICE,
|
||||
}],
|
||||
};
|
||||
// This is semantically equivalent, but not identical. The Metadata data
|
||||
// structure only retains the order of partitions within a group, but not
|
||||
// globally. This checksum is meant to protect against unintended future
|
||||
// changes.
|
||||
let sha512 = [
|
||||
0xfa, 0xdf, 0xf2, 0xb6, 0x74, 0xec, 0x78, 0x7d, 0x0f, 0x7d, 0x17, 0x54, 0xcf, 0x1b, 0x53,
|
||||
0x13, 0x66, 0x13, 0x5e, 0x8e, 0xcc, 0x84, 0xa2, 0x63, 0xaf, 0x0d, 0x68, 0x96, 0xc6, 0x40,
|
||||
0x4e, 0x83, 0xe4, 0xe9, 0xef, 0x61, 0xdc, 0x2a, 0x25, 0x5f, 0xa2, 0x7d, 0x29, 0x0b, 0xb6,
|
||||
0x26, 0x93, 0x59, 0xc9, 0xa8, 0x56, 0x3b, 0x3d, 0x3d, 0x15, 0x6b, 0xee, 0x78, 0x56, 0x78,
|
||||
0xa1, 0x83, 0x6d, 0x70,
|
||||
];
|
||||
|
||||
round_trip(&metadata, &sha512);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_normal_image() {
|
||||
// Layout from Google Pixel 9 Pro XL GrapheneOS factory image:
|
||||
// komodo-install-2024082500.zip -> super_1.img
|
||||
let slot = MetadataSlot {
|
||||
major_version: 10,
|
||||
minor_version: 2,
|
||||
groups: vec![
|
||||
PartitionGroup {
|
||||
name: "default".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: None,
|
||||
partitions: vec![],
|
||||
},
|
||||
PartitionGroup {
|
||||
name: "google_dynamic_partitions_a".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: NonZeroU64::new(8527020032),
|
||||
partitions: vec![
|
||||
Partition {
|
||||
name: "system_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 2465952,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 2048,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "system_dlkm_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 23720,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 2469888,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "system_ext_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 786144,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 2494464,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "product_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 1396432,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 3280896,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 1959024,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 4677632,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_dlkm_a".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 55008,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 6637568,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
PartitionGroup {
|
||||
name: "google_dynamic_partitions_b".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: NonZeroU64::new(8527020032),
|
||||
partitions: vec![
|
||||
Partition {
|
||||
name: "system_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "system_dlkm_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "system_ext_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "product_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor_dlkm_b".into(),
|
||||
attributes: PartitionAttributes::READONLY,
|
||||
extents: vec![],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
block_devices: vec![BlockDevice {
|
||||
first_logical_sector: 2048,
|
||||
alignment: 1048576,
|
||||
alignment_offset: 0,
|
||||
size: 8531214336,
|
||||
partition_name: "super".into(),
|
||||
flags: BlockDeviceFlags::empty(),
|
||||
}],
|
||||
flags: HeaderFlags::VIRTUAL_AB_DEVICE,
|
||||
};
|
||||
let metadata = Metadata {
|
||||
image_type: ImageType::Normal,
|
||||
metadata_max_size: 65536,
|
||||
metadata_slot_count: 3,
|
||||
logical_block_size: 4096,
|
||||
slots: vec![slot; 3],
|
||||
};
|
||||
// This is semantically equivalent, but not identical. The Metadata data
|
||||
// structure only retains the order of partitions within a group, but not
|
||||
// globally. This checksum is meant to protect against unintended future
|
||||
// changes.
|
||||
let sha512 = [
|
||||
0x3b, 0xad, 0xd4, 0x22, 0xa1, 0x5a, 0xc5, 0xdf, 0x72, 0x7d, 0x92, 0x35, 0x04, 0x8a, 0x75,
|
||||
0xd9, 0x33, 0x0d, 0xaa, 0x9e, 0x97, 0xd4, 0x13, 0x28, 0x5e, 0x0f, 0x12, 0x0c, 0xf2, 0xb3,
|
||||
0xdc, 0x35, 0x89, 0x65, 0x40, 0xb0, 0x67, 0xb1, 0x54, 0x09, 0x52, 0x3e, 0x78, 0x3d, 0x3f,
|
||||
0xa7, 0xf7, 0xa0, 0x77, 0xa8, 0xfc, 0xb7, 0x93, 0x19, 0xcd, 0x43, 0xea, 0x9a, 0x74, 0x65,
|
||||
0x54, 0x3c, 0xaa, 0x12,
|
||||
];
|
||||
|
||||
round_trip(&metadata, &sha512);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_retrofit_image() {
|
||||
// Layout from Google Pixel 3a XL stock factory image:
|
||||
// bonito-ota-sp2a.220505.008-37a410d5.zip -> system.img
|
||||
let slot = MetadataSlot {
|
||||
major_version: 10,
|
||||
minor_version: 0,
|
||||
groups: vec![
|
||||
PartitionGroup {
|
||||
name: "default".into(),
|
||||
flags: PartitionGroupFlags::empty(),
|
||||
maximum_size: None,
|
||||
partitions: vec![],
|
||||
},
|
||||
PartitionGroup {
|
||||
name: "google_dynamic_partitions".into(),
|
||||
flags: PartitionGroupFlags::SLOT_SUFFIXED,
|
||||
maximum_size: NonZeroU64::new(4068474880),
|
||||
partitions: vec![
|
||||
Partition {
|
||||
name: "system".into(),
|
||||
attributes: PartitionAttributes::READONLY
|
||||
| PartitionAttributes::SLOT_SUFFIXED,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 1757416,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 2048,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "vendor".into(),
|
||||
attributes: PartitionAttributes::READONLY
|
||||
| PartitionAttributes::SLOT_SUFFIXED,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 991848,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 1761280,
|
||||
block_device_index: 0,
|
||||
},
|
||||
}],
|
||||
},
|
||||
Partition {
|
||||
name: "product".into(),
|
||||
attributes: PartitionAttributes::READONLY
|
||||
| PartitionAttributes::SLOT_SUFFIXED,
|
||||
extents: vec![
|
||||
Extent {
|
||||
num_sectors: 3627008,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 2754560,
|
||||
block_device_index: 0,
|
||||
},
|
||||
},
|
||||
Extent {
|
||||
num_sectors: 538240,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 2048,
|
||||
block_device_index: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Partition {
|
||||
name: "system_ext".into(),
|
||||
attributes: PartitionAttributes::READONLY
|
||||
| PartitionAttributes::SLOT_SUFFIXED,
|
||||
extents: vec![Extent {
|
||||
num_sectors: 490744,
|
||||
extent_type: ExtentType::Linear {
|
||||
start_sector: 540672,
|
||||
block_device_index: 1,
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
block_devices: vec![
|
||||
BlockDevice {
|
||||
first_logical_sector: 2048,
|
||||
alignment: 1048576,
|
||||
alignment_offset: 0,
|
||||
size: 3267362816,
|
||||
partition_name: "system".into(),
|
||||
flags: BlockDeviceFlags::SLOT_SUFFIXED,
|
||||
},
|
||||
BlockDevice {
|
||||
first_logical_sector: 2048,
|
||||
alignment: 1048576,
|
||||
alignment_offset: 0,
|
||||
size: 805306368,
|
||||
partition_name: "vendor".into(),
|
||||
flags: BlockDeviceFlags::SLOT_SUFFIXED,
|
||||
},
|
||||
],
|
||||
flags: HeaderFlags::empty(),
|
||||
};
|
||||
let metadata = Metadata {
|
||||
image_type: ImageType::Normal,
|
||||
metadata_max_size: 65536,
|
||||
metadata_slot_count: 2,
|
||||
logical_block_size: 4096,
|
||||
slots: vec![slot; 2],
|
||||
};
|
||||
// First 274432 bytes of system.img. Unlike the other test cases, this is
|
||||
// identical to the original image because there is only one partition group
|
||||
// with partitions, so the group-level ordering is the same as the global
|
||||
// ordering.
|
||||
let sha512 = [
|
||||
0xb9, 0x97, 0xf5, 0x83, 0x39, 0x37, 0x90, 0x0a, 0xb6, 0x46, 0xdd, 0x27, 0x57, 0xf1, 0xf3,
|
||||
0xbd, 0x8f, 0xc4, 0x63, 0x07, 0x6f, 0xf4, 0x19, 0xc0, 0x02, 0x28, 0x48, 0x99, 0x54, 0xbb,
|
||||
0xb3, 0xbf, 0x67, 0x95, 0xc4, 0xa7, 0x99, 0xf4, 0xa9, 0xc4, 0xf4, 0x1d, 0xf7, 0x59, 0x28,
|
||||
0xeb, 0xbc, 0x85, 0x46, 0xd1, 0x7d, 0x65, 0x0f, 0xbe, 0x21, 0xf6, 0xf2, 0xa2, 0x20, 0x5b,
|
||||
0xda, 0xde, 0xfa, 0x50,
|
||||
];
|
||||
|
||||
round_trip(&metadata, &sha512);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use avbroot::format::sparse::{
|
||||
self, Chunk, ChunkBounds, ChunkData, CrcMode, Header, SparseReader, SparseWriter,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct TestChunk {
|
||||
chunk: Chunk,
|
||||
data: &'static [u8],
|
||||
}
|
||||
|
||||
fn round_trip(block_size: u32, crc32: u32, test_chunks: &[TestChunk], sha512: &[u8; 64]) {
|
||||
let num_blocks = test_chunks.iter().map(|d| d.chunk.bounds.len()).sum();
|
||||
let header = Header {
|
||||
major_version: sparse::MAJOR_VERSION,
|
||||
minor_version: sparse::MINOR_VERSION,
|
||||
block_size,
|
||||
num_blocks,
|
||||
num_chunks: test_chunks.len() as u32,
|
||||
crc32,
|
||||
};
|
||||
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let mut sparse_writer = SparseWriter::new(writer, header).unwrap();
|
||||
|
||||
for test_chunk in test_chunks {
|
||||
sparse_writer.start_chunk(test_chunk.chunk).unwrap();
|
||||
|
||||
if !test_chunk.data.is_empty() {
|
||||
sparse_writer.write_all(test_chunk.data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let writer = sparse_writer.finish().unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
let reader = Cursor::new(&data);
|
||||
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate).unwrap();
|
||||
|
||||
assert_eq!(sparse_reader.header(), header);
|
||||
|
||||
let mut test_chunks_iter = test_chunks.iter();
|
||||
|
||||
while let Some(chunk) = sparse_reader.next_chunk().unwrap() {
|
||||
let test_chunk = test_chunks_iter.next().unwrap();
|
||||
|
||||
assert_eq!(chunk, test_chunk.chunk);
|
||||
|
||||
if !test_chunk.data.is_empty() {
|
||||
let mut buf = vec![];
|
||||
sparse_reader.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, test_chunk.data);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(test_chunks_iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_full_image() {
|
||||
let block_size = 8;
|
||||
let file_crc32 = 0xf6e23567;
|
||||
let test_chunks = [
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 0, end: 1 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 1, end: 1 },
|
||||
data: ChunkData::Crc32(0x88aa689f),
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 1, end: 2 },
|
||||
data: ChunkData::Fill(0x01234567),
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 2, end: 3 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 3, end: 3 },
|
||||
data: ChunkData::Crc32(0xf6e23567),
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
];
|
||||
let sha512 = [
|
||||
0x19, 0x5f, 0xa7, 0xdb, 0x18, 0xc6, 0xb9, 0x0e, 0xce, 0x4b, 0x4f, 0x35, 0x36, 0x79, 0x46,
|
||||
0x02, 0x7a, 0x45, 0x66, 0x63, 0x0e, 0xd9, 0x76, 0x93, 0x2b, 0x88, 0xe2, 0xbc, 0x0b, 0xd9,
|
||||
0x1f, 0x21, 0x51, 0x92, 0x00, 0x2e, 0xe3, 0xa2, 0xff, 0x24, 0xea, 0xef, 0x24, 0xd5, 0x24,
|
||||
0xf0, 0x46, 0xf3, 0x10, 0x32, 0xf4, 0xa6, 0x3b, 0x9d, 0xcd, 0xc5, 0x57, 0xf4, 0xc0, 0xe8,
|
||||
0x01, 0xe8, 0x1d, 0xb3,
|
||||
];
|
||||
|
||||
round_trip(block_size, file_crc32, &test_chunks, &sha512);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_partial_image() {
|
||||
let block_size = 8;
|
||||
let file_crc32 = 0;
|
||||
let test_chunks = [
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 0, end: 1 },
|
||||
data: ChunkData::Hole,
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 1, end: 2 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 2, end: 3 },
|
||||
data: ChunkData::Hole,
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 3, end: 4 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 4, end: 5 },
|
||||
data: ChunkData::Hole,
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
];
|
||||
let sha512 = [
|
||||
0xee, 0x07, 0xc5, 0x4d, 0x85, 0xee, 0x69, 0x91, 0x61, 0x07, 0x10, 0xed, 0xec, 0x13, 0x5e,
|
||||
0xfb, 0xc3, 0x7d, 0xcf, 0x1f, 0x2a, 0x13, 0xf0, 0xb6, 0x85, 0xb4, 0xee, 0xe9, 0xd7, 0xa1,
|
||||
0x12, 0x79, 0x14, 0x16, 0x30, 0x7a, 0x81, 0xf9, 0x4f, 0x72, 0xb2, 0xdd, 0x33, 0xbe, 0x5d,
|
||||
0x55, 0x70, 0xa9, 0xe3, 0x94, 0x29, 0x40, 0x29, 0x8f, 0x35, 0x23, 0xf8, 0x78, 0x7f, 0xfe,
|
||||
0xd6, 0x4b, 0x60, 0x16,
|
||||
];
|
||||
|
||||
round_trip(block_size, file_crc32, &test_chunks, &sha512);
|
||||
}
|
||||
@@ -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,19 @@ ignore = [
|
||||
]
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
include-dev = true
|
||||
unlicensed = "deny"
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"bzip2-1.0.6",
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
copyleft = "allow"
|
||||
default = "deny"
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
@@ -65,14 +63,11 @@ 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/zip",
|
||||
"https://github.com/jongiddy/bzip2-rs",
|
||||
"https://github.com/chenxiaolong/zip2",
|
||||
]
|
||||
|
||||
+11
-6
@@ -14,21 +14,26 @@ avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.0"
|
||||
rsa = "0.9.6"
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = { version = "0.21.0", features = ["serde"] }
|
||||
toml_edit = { version = "0.23.3", 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
|
||||
# https://github.com/zip-rs/zip2/pull/367
|
||||
# https://github.com/zip-rs/zip2/pull/368
|
||||
# For getting the data offset when writing new zip entries.
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
git = "https://github.com/chenxiaolong/zip2"
|
||||
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
static = ["avbroot/static"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+47
-18
@@ -12,6 +12,11 @@ security_patch_level = "2024-01-01"
|
||||
# Google Pixel 7 Pro
|
||||
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
|
||||
|
||||
[profile.pixel_v4_gki.vabc]
|
||||
# CoW v3 is used starting with the Google Pixel 9a.
|
||||
version = "V3"
|
||||
algo = { kind = "Lz4" }
|
||||
|
||||
[profile.pixel_v4_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
@@ -22,7 +27,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,15 +48,23 @@ 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 = "d3436650b4e0c60688dafb1472fa5fe95e67d19a8fad2da4850ffaa739d44574"
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "ef6261cd9ebea90f036e52a46160a400c5b8f6ef24ed2469c4a1e9689987aa06"
|
||||
patched = "37fd353a766a7b9a339fbf51fa79c703e94640dc6a2c6310d79357aaefcc7ca1"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "8a2c717607c10dfa5483d6f9a9f37b3d978acaf8d2ea18e36544af267943e750"
|
||||
patched = "2c4734c9e1d028ee6aaf02bb416e5e173857faffd2ca067366790655147b3afa"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
|
||||
[profile.pixel_v4_non_gki.vabc]
|
||||
version = "V2"
|
||||
algo = { kind = "Lz4" }
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
@@ -77,21 +90,29 @@ 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 = "0f90ae4c26a54a735d48e13e98bea73b6d1c026b0e95fa5b4b26179a1d6bdc86"
|
||||
[profile.pixel_v4_non_gki.hashes_streaming]
|
||||
original = "630220ef813a2b4743d1941179cc9705da86ad4805f1c52341dcb38fbce3d29e"
|
||||
patched = "b725e91751fe58aed20495aecbf9b4bdc14d2799cd88dcbd58f3a3b02b3af15b"
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_seekable]
|
||||
original = "1afbe6867ded345d941098ee7c7fcf94a3df52c50ff96ab8f3a67b2ab957259a"
|
||||
patched = "4357b977249006b101002c961916f962787315a80b8608494c6a1f0cf09cecd1"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
|
||||
[profile.pixel_v3.vabc]
|
||||
version = "V2"
|
||||
algo = { kind = "Gz" }
|
||||
|
||||
[profile.pixel_v3.partitions.boot]
|
||||
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 +133,15 @@ 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 = "0a92969bbd7cb30071a0799eb20028546d1cec3e6ec1ca4d7e1fe7776f1399fc"
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "9b65037343d45211e0f9706929cba34643a9c54274d1b39740c43f45974984e0"
|
||||
patched = "fb23ab9616968b38b96d1e5e6a503154f89aebc1741e89a9e9dfd2c4d9946b05"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "e581934887dd93b8a9d9c3aa5dec1d48aa7e01bf01ac507e8c5fb256b59cbe7d"
|
||||
patched = "669a826abc6d67e7e0b1def724aa7b470461087255c65663d195df1426a355f0"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -126,7 +151,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
|
||||
@@ -143,6 +168,10 @@ avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes]
|
||||
original = "8b38d2d999b5b6e240e894f669e9e2643b3764c108d53bb7b02447da725e7c18"
|
||||
patched = "85b411947145e89cdc7f71b7109a12586f4dbddce3021f0f96172fc465c189d6"
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "f10ee15c900a474cc6bbefa705f272cef42636ea096e75563d2d78f6c4327fd1"
|
||||
patched = "6929f65909037f5550a53982b71e96bdf69ab876bc5e86702c469ed601be8a9a"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "4e863d251b9ff6eaa1511f9c03e9bdb8919650b2e0eaf23e33892a639edafcaf"
|
||||
patched = "9a103222e73df70a097281525546d25c850df2ae7a2ba715aa5dfbbba3f7972b"
|
||||
|
||||
+32
-9
@@ -1,12 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::OsString, path::PathBuf};
|
||||
|
||||
use avbroot::cli::args::{LogFormat, LogLevel};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use avbroot::cli::args::LogFormat;
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use tracing::Level;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ProfileGroup {
|
||||
@@ -69,10 +68,34 @@ pub struct Cli {
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
|
||||
pub log_level: LogLevel,
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
|
||||
pub log_level: Level,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum PassSource {
|
||||
Env,
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct HelperCli {
|
||||
/// Signature algorithm.
|
||||
pub algorithm: String,
|
||||
|
||||
/// Public key.
|
||||
#[arg(value_name = "FILE", value_parser)]
|
||||
pub public_key: PathBuf,
|
||||
|
||||
/// Non-interactive password source.
|
||||
#[arg(value_name = "SOURCE")]
|
||||
pub pass_source: PassSource,
|
||||
|
||||
/// Non-interactive password source value.
|
||||
#[arg(value_name = "VALUE", value_parser)]
|
||||
pub pass_source_value: OsString,
|
||||
}
|
||||
|
||||
+32
-23
@@ -1,15 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use avbroot::format::payload::{CowVersion, VabcAlgo};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::Document;
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Sha256Hash(
|
||||
#[serde(
|
||||
serialize_with = "hex::serialize",
|
||||
@@ -18,7 +17,7 @@ pub struct Sha256Hash(
|
||||
pub [u8; 32],
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OtaInfo {
|
||||
pub device: String,
|
||||
@@ -30,7 +29,7 @@ pub struct OtaInfo {
|
||||
pub security_patch_level: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Avb {
|
||||
pub signed: bool,
|
||||
@@ -41,7 +40,8 @@ pub struct Avb {
|
||||
pub enum RamdiskContent {
|
||||
Init,
|
||||
Otacerts,
|
||||
InitAndOtacerts,
|
||||
FirstStage,
|
||||
DsuKeyDir,
|
||||
Dlkm,
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ pub enum BootVersion {
|
||||
VendorV4,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BootData {
|
||||
pub version: BootVersion,
|
||||
#[serde(default)]
|
||||
pub kernel: bool,
|
||||
#[serde(default)]
|
||||
pub ramdisks: Vec<RamdiskContent>,
|
||||
pub ramdisks: Vec<Vec<RamdiskContent>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -71,19 +71,19 @@ pub enum DmVerityContent {
|
||||
SystemOtacerts,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DmVerityData {
|
||||
pub content: DmVerityContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VbmetaData {
|
||||
pub deps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Data {
|
||||
Boot(BootData),
|
||||
@@ -91,28 +91,37 @@ pub enum Data {
|
||||
Vbmeta(VbmetaData),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Hashes {
|
||||
pub original: Sha256Hash,
|
||||
pub patched: Sha256Hash,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Partition {
|
||||
pub avb: Avb,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Profile {
|
||||
pub partitions: BTreeMap<String, Partition>,
|
||||
pub hashes: Hashes,
|
||||
pub struct VabcSettings {
|
||||
pub version: CowVersion,
|
||||
pub algo: VabcAlgo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Profile {
|
||||
pub vabc: Option<VabcSettings>,
|
||||
pub partitions: BTreeMap<String, Partition>,
|
||||
pub hashes_streaming: Hashes,
|
||||
pub hashes_seekable: Hashes,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub ota_info: OtaInfo,
|
||||
@@ -120,12 +129,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))
|
||||
}
|
||||
|
||||
+443
-185
@@ -1,28 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023 Pascal Roeleven
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023 Pascal Roeleven
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
env,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, Cursor, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
slice,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use avbroot::{
|
||||
cli::ota::{ExtractCli, PatchCli, VerifyCli},
|
||||
crypto::{self, PassphraseSource},
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::{
|
||||
avb::{
|
||||
self, AlgorithmType, ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor,
|
||||
@@ -32,31 +32,32 @@ use avbroot::{
|
||||
self, BootImage, BootImageV0Through2, BootImageV3Through4, RamdiskMeta, V1Extra,
|
||||
V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
|
||||
},
|
||||
compression::{CompressedFormat, CompressedWriter},
|
||||
compression::{CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntry, CpioEntryData},
|
||||
ota::{self, SigningWriter, ZipEntry},
|
||||
ota::{self, SigningWriter, ZipEntry, ZipMode},
|
||||
padding,
|
||||
payload::{self, PayloadHeader, PayloadWriter},
|
||||
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcParams},
|
||||
zip::ZipWriterWrapper,
|
||||
},
|
||||
patch::otacert::{self, OtaCertBuildFlags},
|
||||
protobuf::{
|
||||
build::tools::releasetools::{ota_metadata::OtaType, DeviceState, OtaMetadata},
|
||||
build::tools::releasetools::{DeviceState, OtaMetadata, ota_metadata::OtaType},
|
||||
chromeos_update_engine::{
|
||||
DeltaArchiveManifest, DynamicPartitionGroup, DynamicPartitionMetadata, PartitionUpdate,
|
||||
},
|
||||
},
|
||||
stream::{self, CountingWriter, HashingReader, PSeekFile, Reopen, ToWriter},
|
||||
stream::{self, CountingWriter, FromReader, HashingReader, PSeekFile, Reopen, ToWriter},
|
||||
};
|
||||
use clap::Parser;
|
||||
use rsa::RsaPrivateKey;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use rsa::{BigUint, rand_core::OsRng, traits::PublicKeyParts};
|
||||
use tempfile::TempDir;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{info, info_span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
|
||||
use zip::{CompressionMethod, DateTime, ZipWriter, write::SimpleFileOptions};
|
||||
|
||||
use crate::{
|
||||
cli::{Cli, Command, ListCli, ProfileGroup, TestCli},
|
||||
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
|
||||
config::{
|
||||
Avb, BootData, BootVersion, Config, Data, DmVerityContent, DmVerityData, OtaInfo,
|
||||
Partition, Profile, RamdiskContent, VbmetaData,
|
||||
@@ -97,10 +98,10 @@ fn verify_hash(path: &Path, sha256: &[u8; 32], cancel_signal: &AtomicBool) -> Re
|
||||
fn append_avb(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
avb: Avb,
|
||||
hash_tree: bool,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let image_size = file.seek(SeekFrom::End(0))?;
|
||||
@@ -196,7 +197,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(())
|
||||
}
|
||||
@@ -227,6 +228,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));
|
||||
@@ -245,25 +254,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,10 +296,10 @@ fn create_ramdisk(
|
||||
fn create_boot_image(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
avb: Avb,
|
||||
boot_data: &BootData,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -298,7 +311,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 {
|
||||
@@ -361,17 +374,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.contains(&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(),
|
||||
@@ -407,10 +423,10 @@ fn create_boot_image(
|
||||
fn create_dm_verity_image(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
dm_verity_data: &DmVerityData,
|
||||
avb: Avb,
|
||||
dm_verity_data: DmVerityData,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -436,10 +452,10 @@ fn create_dm_verity_image(
|
||||
fn create_vbmeta_image(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
avb: Avb,
|
||||
vbmeta_data: &VbmetaData,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
) -> Result<()> {
|
||||
let mut descriptors = Vec::new();
|
||||
|
||||
@@ -491,7 +507,7 @@ fn create_vbmeta_image(
|
||||
fn create_partition_images(
|
||||
partitions: &BTreeMap<String, Partition>,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<BTreeMap<String, PSeekFile>> {
|
||||
@@ -522,7 +538,7 @@ fn create_partition_images(
|
||||
create_boot_image(
|
||||
&mut file,
|
||||
name,
|
||||
&partition.avb,
|
||||
partition.avb,
|
||||
data,
|
||||
ota_info,
|
||||
key_avb,
|
||||
@@ -535,8 +551,8 @@ fn create_partition_images(
|
||||
create_dm_verity_image(
|
||||
&mut file,
|
||||
name,
|
||||
&partition.avb,
|
||||
data,
|
||||
partition.avb,
|
||||
*data,
|
||||
ota_info,
|
||||
key_avb,
|
||||
cert_ota,
|
||||
@@ -545,7 +561,7 @@ fn create_partition_images(
|
||||
.with_context(|| format!("Failed to create dm-verity image: {name}"))?;
|
||||
}
|
||||
Data::Vbmeta(data) => {
|
||||
create_vbmeta_image(&mut file, name, &partition.avb, data, &files, key_avb)
|
||||
create_vbmeta_image(&mut file, name, partition.avb, data, &files, key_avb)
|
||||
.with_context(|| format!("Failed to create vbmeta image: {name}"))?;
|
||||
}
|
||||
}
|
||||
@@ -561,9 +577,12 @@ fn create_payload(
|
||||
partitions: &BTreeMap<String, Partition>,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
ota_info: &OtaInfo,
|
||||
key_ota: &RsaPrivateKey,
|
||||
profile: &Profile,
|
||||
key_ota: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
const COMPRESSION_FACTOR: u32 = 64 * 1024;
|
||||
|
||||
let dynamic_partitions_names = partitions
|
||||
.iter()
|
||||
.filter(|(_, p)| matches!(&p.data, Data::DmVerity(_)))
|
||||
@@ -578,11 +597,23 @@ fn create_payload(
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to create temp file for: {name}"))?;
|
||||
|
||||
let (partition_info, operations) =
|
||||
payload::compress_image(file, &writer, name, 4096, cancel_signal)?;
|
||||
let vabc_params = if dynamic_partitions_names.contains(name) {
|
||||
profile.vabc.map(|v| VabcParams {
|
||||
version: v.version,
|
||||
algo: v.algo,
|
||||
compression_factor: COMPRESSION_FACTOR,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (partition_info, operations, cow_estimate) =
|
||||
payload::compress_image(file, &writer, name, 4096, vabc_params, cancel_signal)?;
|
||||
|
||||
compressed.insert(name, writer);
|
||||
|
||||
let is_v3 = profile.vabc.is_some_and(|e| e.version == CowVersion::V3);
|
||||
|
||||
payload_partitions.push(PartitionUpdate {
|
||||
partition_name: name.clone(),
|
||||
run_postinstall: None,
|
||||
@@ -602,7 +633,8 @@ fn create_payload(
|
||||
fec_roots: None,
|
||||
version: None,
|
||||
merge_operations: vec![],
|
||||
estimate_cow_size: None,
|
||||
estimate_cow_size: cow_estimate.map(|e| e.size),
|
||||
estimate_op_count_max: cow_estimate.and_then(|e| is_v3.then_some(e.num_ops)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -622,10 +654,16 @@ fn create_payload(
|
||||
partition_names: dynamic_partitions_names,
|
||||
}],
|
||||
snapshot_enabled: Some(true),
|
||||
vabc_enabled: Some(true),
|
||||
vabc_compression_param: Some("gz".to_owned()),
|
||||
cow_version: Some(2),
|
||||
// Everything below is meant to be unset if VABC is not
|
||||
// supported.
|
||||
vabc_enabled: profile.vabc.map(|_| true),
|
||||
vabc_compression_param: profile.vabc.map(|v| v.algo.to_string()),
|
||||
cow_version: profile.vabc.map(|v| match v.version {
|
||||
CowVersion::V2 => 2,
|
||||
CowVersion::V3 => 3,
|
||||
}),
|
||||
vabc_feature_set: None,
|
||||
compression_factor: profile.vabc.map(|_| COMPRESSION_FACTOR.into()),
|
||||
}),
|
||||
partial_update: None,
|
||||
apex_info: vec![],
|
||||
@@ -666,22 +704,26 @@ fn create_payload(
|
||||
.with_context(|| format!("Failed to copy from image: {name}"))?;
|
||||
}
|
||||
|
||||
let (_, properties, metadata_size) = payload_writer
|
||||
let (_, _, properties, metadata_size) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
Ok((properties, metadata_size))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_ota(
|
||||
output: &Path,
|
||||
ota_info: &OtaInfo,
|
||||
profile: &Profile,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_ota: &RsaPrivateKey,
|
||||
zip_mode: ZipMode,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
info!("Generating OTA: {output:?}");
|
||||
|
||||
let inputs = create_partition_images(
|
||||
&profile.partitions,
|
||||
ota_info,
|
||||
@@ -690,12 +732,25 @@ fn create_ota(
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
let raw_writer =
|
||||
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let buffered_writer = BufWriter::new(raw_writer);
|
||||
let signing_writer = SigningWriter::new(buffered_writer);
|
||||
let mut zip_writer = ZipWriter::new_streaming(signing_writer);
|
||||
let options = FileOptions::default()
|
||||
let raw_writer = OpenOptions::new()
|
||||
.read(zip_mode == ZipMode::Seekable)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output)
|
||||
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let mut zip_writer = match zip_mode {
|
||||
ZipMode::Streaming => {
|
||||
let signing_writer = SigningWriter::new_streaming(raw_writer);
|
||||
ZipWriterWrapper::new_streaming(signing_writer)
|
||||
}
|
||||
ZipMode::Seekable => {
|
||||
let signing_writer = SigningWriter::new_seekable(raw_writer);
|
||||
ZipWriterWrapper::new_seekable(signing_writer)
|
||||
}
|
||||
};
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(CompressionMethod::Stored)
|
||||
.large_file(false);
|
||||
|
||||
@@ -705,17 +760,14 @@ fn create_ota(
|
||||
|
||||
for path in [ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES] {
|
||||
// All remaining entries are written immediately.
|
||||
zip_writer
|
||||
.start_file_with_extra_data(path, options)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let offset = zip_writer
|
||||
.end_extra_data()
|
||||
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
|
||||
.start_file(path, options)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let mut writer = CountingWriter::new(&mut zip_writer);
|
||||
|
||||
match path {
|
||||
ota::PATH_OTACERT => {
|
||||
crypto::write_pem_cert(&mut writer, cert_ota)
|
||||
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
@@ -724,6 +776,7 @@ fn create_ota(
|
||||
&profile.partitions,
|
||||
&inputs,
|
||||
ota_info,
|
||||
profile,
|
||||
key_ota,
|
||||
cancel_signal,
|
||||
)
|
||||
@@ -744,7 +797,7 @@ fn create_ota(
|
||||
let size = writer.stream_position()?;
|
||||
|
||||
entries.push(ZipEntry {
|
||||
name: path.to_owned(),
|
||||
path: path.to_owned(),
|
||||
offset,
|
||||
size,
|
||||
});
|
||||
@@ -778,13 +831,18 @@ fn create_ota(
|
||||
spl_downgrade: false,
|
||||
};
|
||||
|
||||
let data_descriptor_size = match zip_mode {
|
||||
ZipMode::Streaming => 16,
|
||||
ZipMode::Seekable => 0,
|
||||
};
|
||||
ota::add_metadata(
|
||||
&entries,
|
||||
&mut zip_writer,
|
||||
// Offset where next entry would begin.
|
||||
entries.last().map(|e| e.offset + e.size).unwrap() + 16,
|
||||
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
|
||||
&metadata,
|
||||
payload_metadata_size.unwrap(),
|
||||
zip_mode,
|
||||
)
|
||||
.context("Failed to write new OTA metadata")?;
|
||||
|
||||
@@ -792,7 +850,7 @@ fn create_ota(
|
||||
.finish()
|
||||
.context("Failed to finalize output zip")?;
|
||||
let mut buffered_writer = signing_writer
|
||||
.finish(key_ota, cert_ota)
|
||||
.finish(key_ota, cert_ota, cancel_signal)
|
||||
.context("Failed to sign output zip")?;
|
||||
buffered_writer
|
||||
.flush()
|
||||
@@ -805,39 +863,47 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
|
||||
let raw_writer =
|
||||
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let mut zip_writer = ZipWriter::new(raw_writer);
|
||||
let options = SimpleFileOptions::default().last_modified_time(DateTime::default());
|
||||
|
||||
for path in [
|
||||
"assets/stub.apk",
|
||||
"lib/arm64-v8a/libinit-ld.so",
|
||||
"lib/arm64-v8a/libmagisk64.so",
|
||||
"lib/arm64-v8a/libmagiskinit.so",
|
||||
"lib/armeabi-v7a/libinit-ld.so",
|
||||
"lib/armeabi-v7a/libmagisk32.so",
|
||||
"lib/armeabi-v7a/libmagiskinit.so",
|
||||
"lib/x86/libinit-ld.so",
|
||||
"lib/x86/libmagisk32.so",
|
||||
"lib/x86/libmagiskinit.so",
|
||||
"lib/x86_64/libinit-ld.so",
|
||||
"lib/x86_64/libmagisk64.so",
|
||||
"lib/x86_64/libmagiskinit.so",
|
||||
] {
|
||||
zip_writer.start_file(path, FileOptions::default())?;
|
||||
zip_writer.start_file(path, options)?;
|
||||
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.start_file("assets/util_functions.sh", options)?;
|
||||
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct KeySet {
|
||||
avb_key: RsaPrivateKey,
|
||||
ota_key: RsaPrivateKey,
|
||||
avb_key: RsaSigningKey,
|
||||
ota_key: RsaSigningKey,
|
||||
ota_cert: Certificate,
|
||||
avb_key_file: NamedTempFile,
|
||||
avb_pass_file: NamedTempFile,
|
||||
avb_pkmd_file: NamedTempFile,
|
||||
ota_key_file: NamedTempFile,
|
||||
ota_pass_file: NamedTempFile,
|
||||
ota_cert_file: NamedTempFile,
|
||||
_key_dir: TempDir,
|
||||
avb_key_file: PathBuf,
|
||||
avb_public_key_file: PathBuf,
|
||||
avb_pass_file: PathBuf,
|
||||
avb_pkmd_file: PathBuf,
|
||||
ota_key_file: PathBuf,
|
||||
ota_public_key_file: PathBuf,
|
||||
ota_pass_file: PathBuf,
|
||||
ota_cert_file: PathBuf,
|
||||
}
|
||||
|
||||
macro_rules! new_keys_with_prefix {
|
||||
@@ -886,13 +952,8 @@ macro_rules! new_keys_with_prefix {
|
||||
}
|
||||
|
||||
impl KeySet {
|
||||
fn write_temp(data: &[u8]) -> Result<NamedTempFile> {
|
||||
let mut temp_file =
|
||||
NamedTempFile::new().context("Failed to create temp file for test keys")?;
|
||||
temp_file
|
||||
.write_all(data)
|
||||
.with_context(|| format!("Failed to write test key data: {:?}", temp_file.path()))?;
|
||||
Ok(temp_file)
|
||||
fn write(path: &Path, data: &[u8]) -> Result<()> {
|
||||
fs::write(path, data).with_context(|| format!("Failed to write test key data: {path:?}"))
|
||||
}
|
||||
|
||||
fn new_with_data(
|
||||
@@ -903,36 +964,54 @@ impl KeySet {
|
||||
ota_pass: &[u8],
|
||||
ota_cert: &[u8],
|
||||
) -> Result<Self> {
|
||||
let avb_key_file = Self::write_temp(avb_key)?;
|
||||
let avb_pass_file = Self::write_temp(avb_pass)?;
|
||||
let avb_pkmd_file = Self::write_temp(avb_pkmd)?;
|
||||
let ota_key_file = Self::write_temp(ota_key)?;
|
||||
let ota_pass_file = Self::write_temp(ota_pass)?;
|
||||
let ota_cert_file = Self::write_temp(ota_cert)?;
|
||||
let key_dir = TempDir::new().context("Failed to create temp directory")?;
|
||||
let avb_key_file = key_dir.path().join("avb.key");
|
||||
let avb_public_key_file = key_dir.path().join("avb.public.key");
|
||||
let avb_pass_file = key_dir.path().join("avb.passphrase");
|
||||
let avb_pkmd_file = key_dir.path().join("avb_pkmd.bin");
|
||||
let ota_key_file = key_dir.path().join("ota.key");
|
||||
let ota_public_key_file = key_dir.path().join("ota.public.key");
|
||||
let ota_pass_file = key_dir.path().join("ota.passphrase");
|
||||
let ota_cert_file = key_dir.path().join("ota.crt");
|
||||
|
||||
Self::write(&avb_key_file, avb_key)?;
|
||||
Self::write(&avb_pass_file, avb_pass)?;
|
||||
Self::write(&avb_pkmd_file, avb_pkmd)?;
|
||||
Self::write(&ota_key_file, ota_key)?;
|
||||
Self::write(&ota_pass_file, ota_pass)?;
|
||||
Self::write(&ota_cert_file, ota_cert)?;
|
||||
|
||||
let avb_key = crypto::read_pem_key_file(
|
||||
avb_key_file.path(),
|
||||
&PassphraseSource::File(avb_pass_file.path().to_owned()),
|
||||
&avb_key_file,
|
||||
&PassphraseSource::File(avb_pass_file.clone()),
|
||||
)
|
||||
.map(RsaSigningKey::Internal)
|
||||
.context("Failed to load AVB test key")?;
|
||||
|
||||
let ota_key = crypto::read_pem_key_file(
|
||||
ota_key_file.path(),
|
||||
&PassphraseSource::File(ota_pass_file.path().to_owned()),
|
||||
&ota_key_file,
|
||||
&PassphraseSource::File(ota_pass_file.clone()),
|
||||
)
|
||||
.map(RsaSigningKey::Internal)
|
||||
.context("Failed to load OTA test key")?;
|
||||
|
||||
let ota_cert = crypto::read_pem_cert_file(ota_cert_file.path())
|
||||
.context("Failed to load OTA test cert")?;
|
||||
crypto::write_pem_public_key_file(&avb_public_key_file, &avb_key.to_public_key())?;
|
||||
crypto::write_pem_public_key_file(&ota_public_key_file, &ota_key.to_public_key())?;
|
||||
|
||||
let ota_cert =
|
||||
crypto::read_pem_cert_file(&ota_cert_file).context("Failed to load OTA test cert")?;
|
||||
|
||||
Ok(Self {
|
||||
avb_key,
|
||||
ota_key,
|
||||
ota_cert,
|
||||
_key_dir: key_dir,
|
||||
avb_key_file,
|
||||
avb_public_key_file,
|
||||
avb_pass_file,
|
||||
avb_pkmd_file,
|
||||
ota_key_file,
|
||||
ota_public_key_file,
|
||||
ota_pass_file,
|
||||
ota_cert_file,
|
||||
})
|
||||
@@ -942,15 +1021,27 @@ impl KeySet {
|
||||
new_keys_with_prefix!(new_for_test, "TEST_KEY_DO_NOT_USE_");
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn patch_image(
|
||||
input_file: &Path,
|
||||
output_file: &Path,
|
||||
system_image_file: &Path,
|
||||
zip_mode: ZipMode,
|
||||
extra_args: &[&OsStr],
|
||||
keys: &KeySet,
|
||||
signing_helper: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
info!("Patching OTA: {input_file:?} -> {output_file:?}");
|
||||
|
||||
let (avb_key_file, ota_key_file) = if signing_helper {
|
||||
(&keys.avb_public_key_file, &keys.ota_public_key_file)
|
||||
} else {
|
||||
(&keys.avb_key_file, &keys.ota_key_file)
|
||||
};
|
||||
|
||||
let zip_mode_arg = zip_mode.to_string();
|
||||
|
||||
// We're intentionally using the CLI interface.
|
||||
let mut args: Vec<&OsStr> = vec![
|
||||
OsStr::new("patch"),
|
||||
@@ -958,17 +1049,31 @@ fn patch_image(
|
||||
input_file.as_os_str(),
|
||||
OsStr::new("--output"),
|
||||
output_file.as_os_str(),
|
||||
OsStr::new("--replace"),
|
||||
OsStr::new("system"),
|
||||
system_image_file.as_os_str(),
|
||||
OsStr::new("--key-avb"),
|
||||
keys.avb_key_file.path().as_os_str(),
|
||||
avb_key_file.as_os_str(),
|
||||
OsStr::new("--pass-avb-file"),
|
||||
keys.avb_pass_file.path().as_os_str(),
|
||||
keys.avb_pass_file.as_os_str(),
|
||||
OsStr::new("--key-ota"),
|
||||
keys.ota_key_file.path().as_os_str(),
|
||||
ota_key_file.as_os_str(),
|
||||
OsStr::new("--pass-ota-file"),
|
||||
keys.ota_pass_file.path().as_os_str(),
|
||||
keys.ota_pass_file.as_os_str(),
|
||||
OsStr::new("--cert-ota"),
|
||||
keys.ota_cert_file.path().as_os_str(),
|
||||
keys.ota_cert_file.as_os_str(),
|
||||
OsStr::new("--dsu"),
|
||||
OsStr::new("--zip-mode"),
|
||||
OsStr::new(&zip_mode_arg),
|
||||
];
|
||||
|
||||
let argv0: OsString;
|
||||
if signing_helper {
|
||||
argv0 = env::args_os().next().unwrap();
|
||||
args.push(OsStr::new("--signing-helper"));
|
||||
args.push(&argv0);
|
||||
}
|
||||
|
||||
args.extend_from_slice(extra_args);
|
||||
|
||||
let cli = PatchCli::try_parse_from(args)?;
|
||||
@@ -1000,15 +1105,65 @@ fn verify_image(input_file: &Path, keys: &KeySet, cancel_signal: &AtomicBool) ->
|
||||
OsStr::new("--input"),
|
||||
input_file.as_os_str(),
|
||||
OsStr::new("--public-key-avb"),
|
||||
keys.avb_pkmd_file.path().as_os_str(),
|
||||
keys.avb_pkmd_file.as_os_str(),
|
||||
OsStr::new("--cert-ota"),
|
||||
keys.ota_cert_file.path().as_os_str(),
|
||||
keys.ota_cert_file.as_os_str(),
|
||||
])?;
|
||||
avbroot::cli::ota::verify_subcommand(&cli, cancel_signal)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clean_boot_image_certs(path: &Path, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
info!("Removing OTA certs and DSU public keys: {path:?}");
|
||||
|
||||
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
|
||||
let mut boot_image = BootImage::from_reader(&mut file)?;
|
||||
|
||||
let ramdisks = match &mut 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,
|
||||
};
|
||||
|
||||
if ramdisks.is_empty() {
|
||||
bail!("No ramdisk found: {path:?}");
|
||||
}
|
||||
|
||||
let raw_ramdisk_reader = Cursor::new(&ramdisks[0]);
|
||||
let ramdisk_reader = CompressedReader::new(raw_ramdisk_reader, false)?;
|
||||
let ramdisk_format = ramdisk_reader.format();
|
||||
let mut entries = cpio::load(ramdisk_reader, false, cancel_signal)?;
|
||||
|
||||
// Wipe out OTA certificates.
|
||||
if let Some(entry) = entries
|
||||
.iter_mut()
|
||||
.find(|e| e.path == b"system/etc/security/otacerts.zip")
|
||||
{
|
||||
let zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let empty_zip = zip_writer.finish()?.into_inner();
|
||||
|
||||
entry.data = CpioEntryData::Data(empty_zip);
|
||||
}
|
||||
|
||||
// Wipe out DSU public keys.
|
||||
entries.retain(|e| !e.path.starts_with(b"first_stage_ramdisk/avb/"));
|
||||
|
||||
let raw_ramdisk_writer = Cursor::new(Vec::new());
|
||||
let mut ramdisk_writer = CompressedWriter::new(raw_ramdisk_writer, ramdisk_format)?;
|
||||
cpio::save(&mut ramdisk_writer, &entries, false, cancel_signal)?;
|
||||
|
||||
let raw_ramdisk_writer = ramdisk_writer.finish()?;
|
||||
ramdisks[0] = raw_ramdisk_writer.into_inner();
|
||||
|
||||
// We're creating an input for --prepatched, so no need for AVB metadata.
|
||||
file.set_len(0)?;
|
||||
file.rewind()?;
|
||||
boot_image.to_writer(&mut file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn filter_profiles<'a>(config: &'a Config, cli: &'a ProfileGroup) -> Result<BTreeSet<&'a str>> {
|
||||
let mut profiles = config
|
||||
.profile
|
||||
@@ -1047,6 +1202,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
Some(_) => None,
|
||||
None => Some(TempDir::new().context("Failed to create temp directory")?),
|
||||
};
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let work_dir = match &cli.config.work_dir {
|
||||
Some(w) => w.as_path(),
|
||||
None => work_temp_dir.as_ref().unwrap().path(),
|
||||
@@ -1066,84 +1222,103 @@ 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}");
|
||||
}
|
||||
|
||||
info!("Generating OTA");
|
||||
|
||||
let profile = &config.profile[name];
|
||||
|
||||
// Can't used NamedTempFile because avbroot does atomic replaces.
|
||||
let profile_dir = work_dir.join(name);
|
||||
let out_original = profile_dir.join("ota.zip");
|
||||
let out_magisk = profile_dir.join("ota_magisk.zip");
|
||||
let out_prepatched = profile_dir.join("ota_prepatched.zip");
|
||||
for (zip_mode, hashes) in [
|
||||
(ZipMode::Streaming, &profile.hashes_streaming),
|
||||
(ZipMode::Seekable, &profile.hashes_seekable),
|
||||
] {
|
||||
let _span = info_span!("profile", name, %zip_mode).entered();
|
||||
|
||||
fs::create_dir_all(&profile_dir)
|
||||
.with_context(|| format!("Failed to create directory: {profile_dir:?}"))?;
|
||||
// Can't use NamedTempFile because avbroot does atomic replaces.
|
||||
let mut profile_dir = work_dir.join(name);
|
||||
profile_dir.push(zip_mode.to_string());
|
||||
let out_original = profile_dir.join("ota.zip");
|
||||
let out_magisk = profile_dir.join("ota_magisk.zip");
|
||||
let out_prepatched = profile_dir.join("ota_prepatched.zip");
|
||||
|
||||
create_ota(
|
||||
&out_original,
|
||||
&config.ota_info,
|
||||
profile,
|
||||
&orig_keys.avb_key,
|
||||
&orig_keys.ota_key,
|
||||
&orig_keys.ota_cert,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to create OTA"))?;
|
||||
fs::create_dir_all(&profile_dir)
|
||||
.with_context(|| format!("Failed to create directory: {profile_dir:?}"))?;
|
||||
|
||||
verify_image(&out_original, &orig_keys, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify original OTA"))?;
|
||||
create_ota(
|
||||
&out_original,
|
||||
&config.ota_info,
|
||||
profile,
|
||||
zip_mode,
|
||||
&orig_keys.avb_key,
|
||||
&orig_keys.ota_key,
|
||||
&orig_keys.ota_cert,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to create OTA"))?;
|
||||
|
||||
verify_hash(&out_original, &profile.hashes.original.0, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
|
||||
verify_image(&out_original, &orig_keys, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify original OTA"))?;
|
||||
|
||||
// Patch once using Magisk.
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_magisk,
|
||||
&args_magisk,
|
||||
&test_keys,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
|
||||
verify_hash(&out_original, &hashes.original.0, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
|
||||
|
||||
verify_image(&out_magisk, &test_keys, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
|
||||
// Patch once using Magisk.
|
||||
extract_image(&out_original, &profile_dir, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
|
||||
|
||||
verify_hash(&out_magisk, &profile.hashes.patched.0, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
|
||||
let system_image = profile_dir.join("system.img");
|
||||
|
||||
// Patch again, but this time, use the previously patched boot image
|
||||
// instead of applying the Magisk patch.
|
||||
extract_image(&out_magisk, &profile_dir, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_magisk,
|
||||
&system_image,
|
||||
zip_mode,
|
||||
&args_magisk,
|
||||
&test_keys,
|
||||
false,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
|
||||
|
||||
let mut magisk_image = profile_dir.join("init_boot.img");
|
||||
if !magisk_image.exists() {
|
||||
magisk_image = profile_dir.join("boot.img");
|
||||
verify_image(&out_magisk, &test_keys, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
|
||||
|
||||
verify_hash(&out_magisk, &hashes.patched.0, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
|
||||
|
||||
// Patch again, but this time, use the previously patched boot image
|
||||
// instead of applying the Magisk patch.
|
||||
extract_image(&out_magisk, &profile_dir, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
|
||||
|
||||
let mut magisk_image = profile_dir.join("init_boot.img");
|
||||
if !magisk_image.exists() {
|
||||
magisk_image = profile_dir.join("boot.img");
|
||||
}
|
||||
|
||||
// Wipe out changes that we expect to be made to prepatched images.
|
||||
clean_boot_image_certs(&magisk_image, cancel_signal)?;
|
||||
|
||||
let args_prepatched = [OsStr::new("--prepatched"), magisk_image.as_os_str()];
|
||||
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_prepatched,
|
||||
&system_image,
|
||||
zip_mode,
|
||||
&args_prepatched,
|
||||
&test_keys,
|
||||
true,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
|
||||
|
||||
verify_image(&out_prepatched, &test_keys, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
|
||||
|
||||
verify_hash(&out_prepatched, &hashes.patched.0, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
|
||||
}
|
||||
|
||||
let args_prepatched = [OsStr::new("--prepatched"), magisk_image.as_os_str()];
|
||||
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_prepatched,
|
||||
&args_prepatched,
|
||||
&test_keys,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
|
||||
|
||||
verify_image(&out_prepatched, &test_keys, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
|
||||
|
||||
verify_hash(&out_prepatched, &profile.hashes.patched.0, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1159,7 +1334,90 @@ fn list_subcommand(cli: &ListCli) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A basic --signing-helper implementation that just signs via RustCrypto.
|
||||
fn helper_mode() -> Result<()> {
|
||||
let cli = HelperCli::parse();
|
||||
|
||||
let private_key_path = {
|
||||
let parent = cli.public_key.parent().unwrap_or_else(|| Path::new("."));
|
||||
let name = cli
|
||||
.public_key
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.ok_or_else(|| anyhow!("Bad filename: {:?}", cli.public_key))?;
|
||||
|
||||
parent.join(name.replace(".public", ""))
|
||||
};
|
||||
let source = match cli.pass_source {
|
||||
PassSource::Env => PassphraseSource::EnvVar(cli.pass_source_value),
|
||||
PassSource::File => PassphraseSource::File(cli.pass_source_value.into()),
|
||||
};
|
||||
let private_key = crypto::read_pem_key_file(&private_key_path, &source)
|
||||
.with_context(|| format!("Failed to load private key: {private_key_path:?}"))?;
|
||||
let public_key = crypto::read_pem_public_key_file(&cli.public_key)
|
||||
.with_context(|| format!("Failed to load public key: {:?}", cli.public_key))?;
|
||||
|
||||
if private_key.to_public_key() != public_key {
|
||||
bail!("Private key does not match public key");
|
||||
}
|
||||
|
||||
let (hash_algo, key_algo) = cli
|
||||
.algorithm
|
||||
.split_once('_')
|
||||
.ok_or_else(|| anyhow!("Unknown algorithm: {:?}", cli.algorithm))?;
|
||||
|
||||
if key_algo != format!("RSA{}", private_key.size() * 8) {
|
||||
bail!(
|
||||
"{key_algo} does not match key size ({})",
|
||||
private_key.size() * 8
|
||||
);
|
||||
} else if hash_algo != "SHA256" && hash_algo != "SHA512" {
|
||||
bail!("Unknown hash algorithm: {hash_algo}");
|
||||
}
|
||||
|
||||
let mut padded_digest = vec![];
|
||||
io::stdin()
|
||||
.read_to_end(&mut padded_digest)
|
||||
.context("Failed to read padded digest from stdin")?;
|
||||
|
||||
if padded_digest.len() != private_key.size() {
|
||||
bail!(
|
||||
"Padded digest size ({}) bytes does not match key size ({})",
|
||||
padded_digest.len(),
|
||||
private_key.size()
|
||||
);
|
||||
}
|
||||
|
||||
// The input is already padded, so perform a raw RSA signing operation.
|
||||
let mut signature = rsa::hazmat::rsa_decrypt_and_check(
|
||||
&private_key,
|
||||
None::<&mut OsRng>,
|
||||
&BigUint::from_bytes_be(&padded_digest),
|
||||
)
|
||||
.context("Failed to sign digest")?
|
||||
.to_bytes_le();
|
||||
signature.resize(private_key.size(), 0);
|
||||
signature.reverse();
|
||||
|
||||
io::stdout()
|
||||
.write_all(&signature)
|
||||
.context("Failed to write signature to stdout")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
const ENV_HELPER_MODE: &str = "E2E_HELPER_MODE";
|
||||
|
||||
// Re-invoking ourselves will execute as the helper script instead.
|
||||
if env::var_os(ENV_HELPER_MODE).is_some() {
|
||||
return helper_mode();
|
||||
}
|
||||
// SAFETY: No multithreading at this point.
|
||||
unsafe {
|
||||
env::set_var(ENV_HELPER_MODE, "true");
|
||||
}
|
||||
|
||||
// Set up a cancel signal so we can properly clean up any temporary files.
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
|
||||
@@ -16,3 +16,6 @@ publish = false
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
avbroot = { path = "../avbroot" }
|
||||
honggfuzz = "0.5.55"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::{io::Cursor, sync::atomic::AtomicBool};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::{io::Cursor, sync::atomic::AtomicBool};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
use avbroot::{format::lp::Metadata, stream::FromReader};
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let reader = Cursor::new(data);
|
||||
let _ = Metadata::from_reader(reader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
use avbroot::format::sparse::{ChunkData, CrcMode, SparseReader};
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let reader = Cursor::new(data);
|
||||
if let Ok(mut sparse_reader) = SparseReader::new(reader, CrcMode::Ignore) {
|
||||
while let Ok(Some(chunk)) = sparse_reader.next_chunk() {
|
||||
if chunk.data == ChunkData::Data {
|
||||
let _ = io::copy(&mut sparse_reader, &mut io::sink());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
+4
-1
@@ -12,4 +12,7 @@ publish = false
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
toml_edit = "0.21.0"
|
||||
toml_edit = "0.23.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
fmt::{self, Write as _},
|
||||
fs::{self, File},
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
@@ -110,7 +108,7 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
for (link_ref, link) in links {
|
||||
result.push_str(&format!("{link_ref}: {link}\n"));
|
||||
let _ = writeln!(result, "{link_ref}: {link}");
|
||||
}
|
||||
|
||||
fs::write(path, result)?;
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod changelog;
|
||||
mod version;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
@@ -9,9 +7,9 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{Result, bail};
|
||||
use clap::Parser;
|
||||
use toml_edit::{value, Document};
|
||||
use toml_edit::{DocumentMut, value};
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
|
||||
@@ -19,7 +17,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())?;
|
||||
|
||||
Reference in New Issue
Block a user