mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
378 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 10370692d0 | |||
| 73b6e9b177 | |||
| 8d25e28ea3 | |||
| 82fc1336f7 | |||
| 953c0c1c5b | |||
| c3b073cece | |||
| 565efc5ee5 | |||
| 6ef8e548a3 | |||
| 80549346ea | |||
| 275b18b2a5 | |||
| 0a68966594 | |||
| 9ef5629556 | |||
| 1db09eb896 | |||
| a134d7f889 | |||
| 833ca931e8 | |||
| 9b6677d28a | |||
| 12e59fcf53 | |||
| 4bc7363f70 | |||
| b5ad9dddf6 | |||
| f0aba89549 | |||
| c9633682ef | |||
| da17935972 | |||
| c2140a85e9 | |||
| 3a3582ce4c | |||
| b029116a5a | |||
| af1b01646d | |||
| e85102709f | |||
| 124629dc45 | |||
| e89c4bdc9e | |||
| c1095b8321 | |||
| 17504a7f81 | |||
| d8190143bf | |||
| 0944abcea2 | |||
| 249c36d8c3 | |||
| 446e87289f | |||
| 04b8e3c315 | |||
| 03809a1e88 | |||
| 1fcb29532d | |||
| b383c8a0ab | |||
| 7ea8e8fa9e | |||
| ce43b3c657 | |||
| 05e5e59d65 | |||
| 72b8120bf9 | |||
| 8110f82b17 | |||
| 64dbfa241e | |||
| 7041c8ef2c | |||
| 7296eed51f | |||
| 698810d256 | |||
| acf57272a7 | |||
| 2b50e4527f | |||
| cd7b5ab23c | |||
| f73c451d23 | |||
| a801e18595 | |||
| 3932ff9092 | |||
| 1a36fbd46c | |||
| 077a80f4ce | |||
| 1e43930f3c | |||
| d00e53fb2a | |||
| 0b5f3b30cf | |||
| 10c2a0969f | |||
| 83218a747d | |||
| 6fb7d568cd | |||
| 6cbf690e17 | |||
| e097f98404 | |||
| d5605eabc0 | |||
| d1bcf2da80 | |||
| bbf8e28a28 | |||
| dc93b17888 | |||
| f83a408dc4 | |||
| 798fddc4d5 | |||
| dfed06dcf8 | |||
| 02fd92a640 | |||
| dd4faf72ae | |||
| adbe249edb | |||
| b7028b13a2 | |||
| 8122b0eece | |||
| 0613de4ee4 | |||
| 1a18eb7f85 | |||
| 1e1ca9dcbf | |||
| 6924783f48 | |||
| 98071daa33 | |||
| 8e1adae947 | |||
| d6705f4f00 | |||
| a648df1695 | |||
| a7c872be3e | |||
| cf1ab6ecca | |||
| 7364e8d725 | |||
| 935a86e72c | |||
| a9a6107043 | |||
| 3c1d5a8bb3 | |||
| 26ec8098e5 | |||
| 2a293147b1 | |||
| aea12c8d58 | |||
| 5545b0fe1b | |||
| 9c818fb165 | |||
| d174af8969 | |||
| 2cf11094de | |||
| 6f565969b7 | |||
| da2eb6b717 | |||
| 2f8d0264eb | |||
| ec1fe74900 | |||
| b3b7c7b738 | |||
| 2f1ee1ae4f | |||
| cb62996b9f | |||
| 54c06835c5 | |||
| 65a1b80da5 | |||
| 76ff1ddcda | |||
| 0a4dda14cd | |||
| bde8dbfbcd | |||
| fbafbafe90 | |||
| a14cab71e5 | |||
| c2f4297bb7 | |||
| 9d63c9759d | |||
| 73b893f3ed | |||
| fd7bbf5751 | |||
| 409867a8e5 | |||
| f96a2887df | |||
| 8140e25620 | |||
| ce05477ab1 | |||
| 1c4800c0cb | |||
| d402fdf5f1 | |||
| 4930527598 | |||
| 9b970c6d25 | |||
| e7fb4004cd | |||
| 2773dcddcf | |||
| 36765290fd | |||
| 6d8268e472 | |||
| 2e8e86766b | |||
| 8ae1c54c13 | |||
| 5212d4de51 | |||
| 61bb9d6a81 | |||
| 6d86fbf8a0 | |||
| 621ccc254c | |||
| f41ef844e0 | |||
| 42f8769e48 | |||
| d2187676d1 | |||
| 3de5194c46 | |||
| e0c1d4ad3a | |||
| c94af49009 | |||
| 5ed7dd7dca | |||
| dac4e8d328 | |||
| 3cc078ccfb | |||
| b47b51114f | |||
| 09048a00d2 | |||
| ca0e80521f | |||
| 6491106df0 | |||
| 2e8fa31207 | |||
| d69fdaadae | |||
| f0a631951a | |||
| 83265e584a | |||
| ddb8911efd | |||
| 72be84b2fc | |||
| 81d77d8610 | |||
| 4651b755ba | |||
| 979699e3dc | |||
| beee2c7c90 | |||
| 206916e544 | |||
| aaed44ad73 | |||
| 37b33f803a | |||
| e72b55dc57 | |||
| 5b01feb344 | |||
| 2f7556b530 | |||
| f25196f0a2 | |||
| 2735a6558e | |||
| 0c03fceb96 | |||
| 164274eb97 | |||
| 61129e8ec7 | |||
| e1f4fa1ee8 | |||
| d15a29e9a7 | |||
| 26a3bafc3d | |||
| 999e31cdf3 | |||
| ae2ebbf2aa | |||
| fea860a382 | |||
| 98339aee13 | |||
| f8772ef327 | |||
| 7d62ddc64e | |||
| d93378cc24 | |||
| 4885e978ee | |||
| ecb2caf47d | |||
| 525e2a409e | |||
| 7e89e9081a | |||
| 2a57343094 | |||
| cd779e62cb | |||
| 96fb8ddc9c | |||
| 0aba185c85 | |||
| 36dcbe092f | |||
| aab9f5216f | |||
| 2a2515c5ef | |||
| 111ec6ff1e | |||
| cfd1dfb2eb | |||
| d7977bab93 | |||
| c29d78181a | |||
| 42a9a9a12b | |||
| e293331952 | |||
| bf97444f4b | |||
| 1fa068b465 | |||
| f85b6eec46 | |||
| 07f2e30430 | |||
| 9f34198f07 | |||
| 9d8e82651e | |||
| 5b22d7b603 | |||
| 11d19de0a1 | |||
| fc4c2e63da | |||
| ee28a57a74 | |||
| 101512b894 | |||
| 50913db375 | |||
| 699ea92dbf | |||
| 667b99254c | |||
| e41814a425 | |||
| dff2fff028 | |||
| 67bca2e58c | |||
| 1f08cb6d40 | |||
| ed68f09cb9 | |||
| 64c11c2dc3 | |||
| 5115134b5a | |||
| a2d81d6bfe | |||
| c5709b90f6 | |||
| 2928b96f1f | |||
| 5638a8bb4d | |||
| a582d0b3e6 | |||
| 1d08ffa527 | |||
| a95ce56246 | |||
| ca6ce1fb37 | |||
| d7a46dcaaf | |||
| a4978aaac0 | |||
| 6ca4023b0d | |||
| acc3308d37 | |||
| 9ca3a690df | |||
| 335c04711e | |||
| fb7be77480 | |||
| e683fce4ab | |||
| 7493cdc5fa | |||
| 8f49f49154 | |||
| bc6fdd8513 | |||
| f6c86473c3 | |||
| d0d51f562c | |||
| f31c4134e4 | |||
| dd0d7f334b | |||
| e4a2a4c24c | |||
| cc9aab197c | |||
| 92da30f53c | |||
| 26142b0271 | |||
| c8df3286df | |||
| 65979beff4 | |||
| 47cf015f15 | |||
| e40e036159 | |||
| 0d2a158ead | |||
| ddabb19b4a | |||
| 3c05eb300c | |||
| 2ffb1dfdbd | |||
| f93dc55f70 | |||
| 393e65f6b4 | |||
| 3f86a67038 | |||
| 4337170f57 | |||
| a73365592f | |||
| 0018e8a5ba | |||
| adf0014da2 |
@@ -1,48 +0,0 @@
|
||||
name: Preload img cache
|
||||
inputs:
|
||||
cache-key-prefix:
|
||||
description: 'Device cache-key prefix'
|
||||
required: true
|
||||
device:
|
||||
description: 'Device name'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/cache@v3
|
||||
id: cache-img
|
||||
with:
|
||||
key: ${{ inputs.cache-key-prefix }}${{ inputs.device }}
|
||||
# Make sure any changes to path are also reflected in ci.yml setup
|
||||
path: e2e/files/${{ inputs.device }}-sparse.tar
|
||||
|
||||
- if: ${{ steps.cache-img.outputs.cache-hit }}
|
||||
name: Extracting image from sparse archive
|
||||
shell: sh
|
||||
working-directory: e2e/files
|
||||
run: tar -xf ${{ inputs.device }}-sparse.tar
|
||||
|
||||
- name: Restore e2e executable
|
||||
if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
- name: Downloading device image for ${{ inputs.device }}
|
||||
if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
shell: sh
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e download --stripped -d ${{ inputs.device }}
|
||||
|
||||
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
name: Creating sparse archive from image
|
||||
shell: sh
|
||||
working-directory: e2e/files
|
||||
run: |
|
||||
tar --sparse -cf ${{ inputs.device }}-sparse.tar \
|
||||
${{ inputs.device }}/*.stripped
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Preload Magisk
|
||||
inputs:
|
||||
cache-key:
|
||||
description: 'Magisk cache-key'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/cache@v3
|
||||
id: cache-magisk
|
||||
with:
|
||||
key: ${{ inputs.cache-key }}
|
||||
# Make sure any changes to path are also reflected in ci.yml setup
|
||||
path: e2e/files/magisk
|
||||
|
||||
- name: Restore e2e executable
|
||||
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
- name: Downloading Magisk
|
||||
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
shell: sh
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e download --magisk
|
||||
+117
-169
@@ -5,31 +5,68 @@ on:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs, but
|
||||
# only in pull requests.
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
group: '${{ github.workflow }} @ ${{ github.head_ref || github.sha }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.artifact.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -C strip=symbols
|
||||
# https://github.com/rust-lang/rust/issues/78210
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
ANDROID_API: ${{ matrix.artifact.android_api }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
artifact:
|
||||
- os: ubuntu-latest
|
||||
name: x86_64-unknown-linux-gnu
|
||||
- os: windows-latest
|
||||
name: x86_64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
name: universal-apple-darwin
|
||||
targets:
|
||||
- aarch64-apple-darwin
|
||||
- x86_64-apple-darwin
|
||||
combine: lipo
|
||||
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
|
||||
- os: ubuntu-24.04
|
||||
name: aarch64-linux-android31
|
||||
targets:
|
||||
- aarch64-linux-android
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install qemu-user-static
|
||||
if: ${{ contains(matrix.artifact.name, 'android') }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user-static
|
||||
|
||||
- name: Set Android temporary directory
|
||||
if: ${{ contains(matrix.artifact.name, 'android') }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "TMPDIR=/tmp" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install cargo-android
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install \
|
||||
--git https://github.com/chenxiaolong/cargo-android \
|
||||
--tag v0.1.1
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
@@ -39,179 +76,90 @@ jobs:
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Get Rust LLVM target triple
|
||||
id: get_target
|
||||
- name: Install toolchains
|
||||
shell: bash
|
||||
env:
|
||||
RUSTC_BOOTSTRAP: '1'
|
||||
run: |
|
||||
echo -n 'name=' >> "${GITHUB_OUTPUT}"
|
||||
rustc -Z unstable-options --print target-spec-json \
|
||||
| jq -r '."llvm-target"' \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
for target in ${TARGETS}; do
|
||||
rustup target add "${target}"
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
clippy --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
build --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
- name: Tests
|
||||
run: cargo test --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
test --release --workspace --features static \
|
||||
--target "${target}"
|
||||
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
|
||||
- name: End to end tests
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
run --release -p e2e --features static \
|
||||
--target "${target}" \
|
||||
-- test -a -c e2e/e2e.toml
|
||||
done
|
||||
|
||||
- name: Create output directory
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf target/output
|
||||
|
||||
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/
|
||||
|
||||
# This is separate so we can have a flat directory structure.
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
target/release/avbroot
|
||||
target/release/avbroot.exe
|
||||
|
||||
- name: Cache e2e executable
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
setup:
|
||||
name: Prepare workflow data
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
config-path: ${{ steps.load-config.outputs.config-path }}
|
||||
device-list: ${{ steps.load-config.outputs.device-list }}
|
||||
magisk-key: ${{ steps.cache-keys.outputs.magisk-key }}
|
||||
img-key-prefix: ${{ steps.cache-keys.outputs.img-key-prefix }}
|
||||
img-hit: ${{ steps.get-img-cache.outputs.cache-matched-key }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Restore e2e executable
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
- name: Loading test config
|
||||
id: load-config
|
||||
working-directory: e2e
|
||||
run: |
|
||||
echo 'config-path=e2e/e2e.toml' >> "${GITHUB_OUTPUT}"
|
||||
echo -n 'device-list=' >> "${GITHUB_OUTPUT}"
|
||||
../target/release/e2e list \
|
||||
| jq -cnR '[inputs | select(length > 0)]' \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Generating cache keys
|
||||
id: cache-keys
|
||||
run: |
|
||||
{
|
||||
echo "img-key-prefix=img-${{ hashFiles(steps.load-config.outputs.config-path) }}-"; \
|
||||
echo "magisk-key=magisk-${{ hashFiles(steps.load-config.outputs.config-path) }}";
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checking for cached device images
|
||||
id: get-img-cache
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: ${{ steps.cache-keys.outputs.img-key-prefix }}
|
||||
lookup-only: true
|
||||
path: |
|
||||
e2e/files/${{ fromJSON(steps.load-config.outputs.device-list)[0] }}-sparse.tar
|
||||
|
||||
- name: Checking for cached magisk apk
|
||||
id: get-magisk-cache
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: ${{ steps.cache-keys.outputs.magisk-key }}
|
||||
lookup-only: true
|
||||
path: e2e/files/magisk
|
||||
|
||||
- name: Preloading Magisk cache
|
||||
if: ${{ ! steps.get-magisk-cache.outputs.cache-hit }}
|
||||
uses: ./.github/actions/preload-magisk-cache
|
||||
with:
|
||||
cache-key: ${{ steps.cache-keys.outputs.magisk-key }}
|
||||
|
||||
preload-img:
|
||||
name: Preload device images
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
timeout-minutes: 5
|
||||
# Assume that preloading always succesfully cached all images before.
|
||||
# If for some reason only some got cached, on the first run, the cache will not be preloaded
|
||||
# which will result in some being downloaded multiple times when running the tests.
|
||||
if: ${{ ! needs.setup.outputs.img-hit }}
|
||||
strategy:
|
||||
matrix:
|
||||
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Preloading image cache
|
||||
uses: ./.github/actions/preload-img-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
|
||||
device: ${{ matrix.device }}
|
||||
|
||||
tests:
|
||||
name: Run test for ${{ matrix.device }} on ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- setup
|
||||
- preload-img
|
||||
timeout-minutes: 10
|
||||
# Continue on skipped but not on failures or cancels
|
||||
if: ${{ always() && ! failure() && ! cancelled() }}
|
||||
strategy:
|
||||
matrix:
|
||||
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Restoring Magisk cache
|
||||
uses: ./.github/actions/preload-magisk-cache
|
||||
with:
|
||||
cache-key: ${{ needs.setup.outputs.magisk-key }}
|
||||
|
||||
- name: Restoring image cache
|
||||
uses: ./.github/actions/preload-img-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
|
||||
device: ${{ matrix.device }}
|
||||
|
||||
- name: Restore e2e executable
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
|
||||
# Finally run tests
|
||||
- name: Run test for ${{ matrix.device }}
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e test --stripped -d ${{ matrix.device }}
|
||||
target/output/LICENSE
|
||||
target/output/README.md
|
||||
target/output/avbroot
|
||||
target/output/avbroot.exe
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: cargo-deny
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -10,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
name: Build modules
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
echo -n 'version=' >> "${GITHUB_OUTPUT}"
|
||||
git describe --always \
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build modules
|
||||
run: cargo xtask modules -a
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: avbroot-modules-${{ steps.get_version.outputs.version }}
|
||||
path: modules/dist/
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: Github Release
|
||||
on:
|
||||
push:
|
||||
# Uncomment to test against a branch
|
||||
@@ -24,14 +25,12 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
body_path: RELEASE.md
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
+366
-3
@@ -1,15 +1,378 @@
|
||||
<!--
|
||||
When adding new changelog entries, use [Issue #0] to link to issues and
|
||||
[PR #0 @user] to link to pull requests. Then run:
|
||||
[PR #0] to link to pull requests. Then run:
|
||||
|
||||
cargo xtask update-changelog
|
||||
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### 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])
|
||||
* https://github.com/chenxiaolong/OEMUnlockOnBoot
|
||||
* The new module supports the automatic update mechanism within Magisk/KernelSU
|
||||
* Add support for Magisk v27.0 ([PR #255])
|
||||
* Switch to using a proper logging library ([PR #251])
|
||||
* Folks who want to see the juicy details during patching can use `--log-level debug` or `--log-level trace`
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Switch from xz2 to liblzma (maintained fork of xz2) ([PR #247])
|
||||
* Update all dependencies ([PR #256])
|
||||
|
||||
### Version 3.0.0
|
||||
|
||||
Happy New Year! This release brings two major changes:
|
||||
|
||||
1. The OTA certificates (`otacerts.zip`) in the system partition are now patched. The `clearotacerts` module from avbroot (or the `customotacerts` module from Custota) are no longer needed and can be safely uninstalled.
|
||||
|
||||
This makes it possible to use Pixel's new Repair Mode safely. To do so, follow the instructions in the [documentation here](./README.md#repair-mode).
|
||||
|
||||
2. Autodetection for boot partitions is now significantly more reliable. For KernelSU users or folks who have more obscure devices, the `--boot-partition` option is no longer required (and is now ignored).
|
||||
|
||||
Full list of changes:
|
||||
|
||||
* Add support for AVB 2.0 format 1.3.0 (for Android 15) ([PR #210])
|
||||
* Add new `avbroot key decode-avb` command for converting AVB-encoded public keys to the standard PKCS8-encoded format ([PR #219])
|
||||
* Improve autodetection of boot images ([Issue #218], [PR #221], [PR #237])
|
||||
* Build precompiled executables as statically linked executables ([Issue #222], [PR #224], [PR #227])
|
||||
* Limit critical partition check to bootloader-verified partitions ([Issue #223], [PR #226])
|
||||
* Improve patching performance by spliiting new partition images into chunks and compressing them in parallel ([PR #228])
|
||||
* Also verify whole-partition hashes when running `avbroot ota verify` ([PR #229])
|
||||
* Add support for patching `otacerts.zip` on the system partition ([Issue #225], [PR #240], [PR #244])
|
||||
* Document how to use Repair Mode safely ([Issue #216], [PR #243])
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Fix lint warnings introduced in Rust 1.74.0 ([PR #211])
|
||||
* Temporarily silence [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071) warning in cargo-deny ([PR #214])
|
||||
* Add support for partially updating FEC data ([PR #230], [PR #231], [PR #234])
|
||||
* Fix hash tree calculation for images smaller than one block ([PR #232])
|
||||
* Refactor hash tree code and add tests, CLI commands, and support for partial updates ([PR #233])
|
||||
* Generate mock OTAs to use for end-to-end tests ([PR #241])
|
||||
* Update all dependencies ([PR #245])
|
||||
|
||||
### Version 2.3.3
|
||||
|
||||
* Add support for XZ-compressed ramdisks ([Issue #203], [PR #207])
|
||||
* Merge property and kernel command line AVB descriptors when replacing partitions ([Issue #203], [PR #208])
|
||||
|
||||
### Version 2.3.2
|
||||
|
||||
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
|
||||
* Automatically update legacy `dm=` kernel command line descriptor when packing AVB images ([Issue #203], [PR #205])
|
||||
* Automatically promote insecure hash algorithms (eg. sha1) to sha256 when packing AVB images ([Issue #203], [PR #206])
|
||||
|
||||
### Version 2.3.1
|
||||
|
||||
* Mark Magisk 264xx as supported ([PR #199])
|
||||
|
||||
### Version 2.3.0
|
||||
|
||||
* Fix missing `--help` text for `avbroot avb unpack`'s `--ignore-invalid` option ([PR #183])
|
||||
* Group `avbroot ota patch --help` output into more readable sections ([PR #184])
|
||||
* Add more checks to ensure that the OTA has a secure AVB setup ([PR #188])
|
||||
* OTAs with blatantly insecure or missing AVB configuration are now more likely to be rejected by avbroot to avoid providing a false sense of security.
|
||||
* Allow `avbroot avb verify` and `avbroot ota verify` to work for dm-verity partitions that use insecure SHA1 hashes ([PR #190])
|
||||
* Add support for legacy Android 11 OTAs ([Discussion #195], [PR #196])
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Bump maximum payload manifest size to 4 MiB ([PR #182])
|
||||
* Rework file handle reopen functionality to use traits instead of callbacks ([PR #189])
|
||||
* Don't set signature algorithm field for indirectly signed boot images ([PR #191])
|
||||
* Update dependencies ([PR #197])
|
||||
|
||||
### Version 2.2.0
|
||||
|
||||
It's Android 14 release day! All versions of avbroot, including the old Python version, are compatible with Android 14 OTAs.
|
||||
|
||||
Changes:
|
||||
|
||||
* Add new unpack and pack commands for cpio archives (ramdisks) ([PR #173], [PR #178])
|
||||
* Rename `header.toml` to `boot.toml` for the boot image unpack and pack commands ([PR #175])
|
||||
* Also changes the file format a bit to make it more readable.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Add streaming CPIO reader and writer ([PR #172])
|
||||
* Update dependencies ([PR #174], [PR #181])
|
||||
* Switch to prost for protobuf encoding/decoding ([PR #176])
|
||||
|
||||
### Version 2.1.1
|
||||
|
||||
This release is all about hardening avbroot against untrusted (or corrupted) inputs. While all of avbroot's parsers are memory-safe, it's still possible for crashes to occur due to logic issues like allocating too much memory or dividing by zero. With this release, most of these potential issues have been fixed and fuzz tests have been added to help find more of these situations.
|
||||
|
||||
On the filesystem side of things, it is no longer possible for a nefarious program to cause avbroot to write to unintended locations by eg. swapping out an output directory or temp directory with a symlink while it is running.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Consolidate logic for handling `--pass-file` and `--pass-env-var` ([PR #156])
|
||||
* cargo-deny: Block executables in dependencies ([PR #133])
|
||||
* Implement size limits for parsers to prevent allocating too much memory ([Issue #157], [PR #158], [PR #159], [PR #164], [PR #168], [PR #169], [PR #170])
|
||||
* Add fuzzers to help catch panics/crashes ([Issue #160], [PR #161], [PR #162], [PR #163], [PR #165], [PR #167])
|
||||
* Use handle-based directory operations instead of path-based directory operations ([Issue #166], [PR #171])
|
||||
|
||||
### Version 2.1.0
|
||||
|
||||
* Add support for dm-verify FEC (forward error correction) ([Issue #145], [PR #146])
|
||||
* `ota verify` and `avb verify` will now check the FEC data.
|
||||
* Print status and warning messages to stderr ([PR #149])
|
||||
* Add new `avb unpack`, `avb pack`, and `avb repack` commands for AVB images ([Issue #144], [Issue #148], [PR #152])
|
||||
* `avb verify` now optionally accepts `--repair` to fix corrupted dm-verity images.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Remove unnecessary use of `Arc` ([PR #147])
|
||||
* Use bstr crate to escape mostly UTF-8 binary data ([PR #150])
|
||||
* Improve error fields and error contest ([PR #153])
|
||||
|
||||
### Version 2.0.3
|
||||
|
||||
* Upgrade xz version in precompiled binaries ([Issue #138], [PR #139])
|
||||
* This fixes the `ota extract` and `ota verify` commands in some multithreaded situations.
|
||||
* Add `--version` option to print out avbroot's version ([Issue #138], [PR #140])
|
||||
|
||||
### Version 2.0.2
|
||||
|
||||
* Fix `data_offset` being set for payload operations that don't need it ([PR #136])
|
||||
* This fixes patched stock OnePlus images from being rejected when flashing.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Move full OTA check to CLI functions to allow library functions to parse delta OTAs ([PR #135])
|
||||
* Remove unnecessary use of `anyhow` macro ([PR #137])
|
||||
|
||||
### Version 2.0.1
|
||||
|
||||
* Add support for Magisk 263xx ([PR #132])
|
||||
|
||||
### Version 2.0.0
|
||||
|
||||
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130 @chenxiaolong])
|
||||
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130])
|
||||
|
||||
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
|
||||
[PR #130 @chenxiaolong]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
[Discussion #195]: https://github.com/chenxiaolong/avbroot/discussions/195
|
||||
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
|
||||
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
|
||||
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
|
||||
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
|
||||
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
|
||||
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
|
||||
[Issue #148]: https://github.com/chenxiaolong/avbroot/issues/148
|
||||
[Issue #157]: https://github.com/chenxiaolong/avbroot/issues/157
|
||||
[Issue #160]: https://github.com/chenxiaolong/avbroot/issues/160
|
||||
[Issue #166]: https://github.com/chenxiaolong/avbroot/issues/166
|
||||
[Issue #201]: https://github.com/chenxiaolong/avbroot/issues/201
|
||||
[Issue #203]: https://github.com/chenxiaolong/avbroot/issues/203
|
||||
[Issue #216]: https://github.com/chenxiaolong/avbroot/issues/216
|
||||
[Issue #218]: https://github.com/chenxiaolong/avbroot/issues/218
|
||||
[Issue #222]: https://github.com/chenxiaolong/avbroot/issues/222
|
||||
[Issue #223]: https://github.com/chenxiaolong/avbroot/issues/223
|
||||
[Issue #225]: https://github.com/chenxiaolong/avbroot/issues/225
|
||||
[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
|
||||
[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
|
||||
[PR #135]: https://github.com/chenxiaolong/avbroot/pull/135
|
||||
[PR #136]: https://github.com/chenxiaolong/avbroot/pull/136
|
||||
[PR #137]: https://github.com/chenxiaolong/avbroot/pull/137
|
||||
[PR #139]: https://github.com/chenxiaolong/avbroot/pull/139
|
||||
[PR #140]: https://github.com/chenxiaolong/avbroot/pull/140
|
||||
[PR #146]: https://github.com/chenxiaolong/avbroot/pull/146
|
||||
[PR #147]: https://github.com/chenxiaolong/avbroot/pull/147
|
||||
[PR #149]: https://github.com/chenxiaolong/avbroot/pull/149
|
||||
[PR #150]: https://github.com/chenxiaolong/avbroot/pull/150
|
||||
[PR #152]: https://github.com/chenxiaolong/avbroot/pull/152
|
||||
[PR #153]: https://github.com/chenxiaolong/avbroot/pull/153
|
||||
[PR #156]: https://github.com/chenxiaolong/avbroot/pull/156
|
||||
[PR #158]: https://github.com/chenxiaolong/avbroot/pull/158
|
||||
[PR #159]: https://github.com/chenxiaolong/avbroot/pull/159
|
||||
[PR #161]: https://github.com/chenxiaolong/avbroot/pull/161
|
||||
[PR #162]: https://github.com/chenxiaolong/avbroot/pull/162
|
||||
[PR #163]: https://github.com/chenxiaolong/avbroot/pull/163
|
||||
[PR #164]: https://github.com/chenxiaolong/avbroot/pull/164
|
||||
[PR #165]: https://github.com/chenxiaolong/avbroot/pull/165
|
||||
[PR #167]: https://github.com/chenxiaolong/avbroot/pull/167
|
||||
[PR #168]: https://github.com/chenxiaolong/avbroot/pull/168
|
||||
[PR #169]: https://github.com/chenxiaolong/avbroot/pull/169
|
||||
[PR #170]: https://github.com/chenxiaolong/avbroot/pull/170
|
||||
[PR #171]: https://github.com/chenxiaolong/avbroot/pull/171
|
||||
[PR #172]: https://github.com/chenxiaolong/avbroot/pull/172
|
||||
[PR #173]: https://github.com/chenxiaolong/avbroot/pull/173
|
||||
[PR #174]: https://github.com/chenxiaolong/avbroot/pull/174
|
||||
[PR #175]: https://github.com/chenxiaolong/avbroot/pull/175
|
||||
[PR #176]: https://github.com/chenxiaolong/avbroot/pull/176
|
||||
[PR #178]: https://github.com/chenxiaolong/avbroot/pull/178
|
||||
[PR #181]: https://github.com/chenxiaolong/avbroot/pull/181
|
||||
[PR #182]: https://github.com/chenxiaolong/avbroot/pull/182
|
||||
[PR #183]: https://github.com/chenxiaolong/avbroot/pull/183
|
||||
[PR #184]: https://github.com/chenxiaolong/avbroot/pull/184
|
||||
[PR #188]: https://github.com/chenxiaolong/avbroot/pull/188
|
||||
[PR #189]: https://github.com/chenxiaolong/avbroot/pull/189
|
||||
[PR #190]: https://github.com/chenxiaolong/avbroot/pull/190
|
||||
[PR #191]: https://github.com/chenxiaolong/avbroot/pull/191
|
||||
[PR #196]: https://github.com/chenxiaolong/avbroot/pull/196
|
||||
[PR #197]: https://github.com/chenxiaolong/avbroot/pull/197
|
||||
[PR #199]: https://github.com/chenxiaolong/avbroot/pull/199
|
||||
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
|
||||
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
|
||||
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
|
||||
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
|
||||
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
|
||||
[PR #210]: https://github.com/chenxiaolong/avbroot/pull/210
|
||||
[PR #211]: https://github.com/chenxiaolong/avbroot/pull/211
|
||||
[PR #214]: https://github.com/chenxiaolong/avbroot/pull/214
|
||||
[PR #219]: https://github.com/chenxiaolong/avbroot/pull/219
|
||||
[PR #221]: https://github.com/chenxiaolong/avbroot/pull/221
|
||||
[PR #224]: https://github.com/chenxiaolong/avbroot/pull/224
|
||||
[PR #226]: https://github.com/chenxiaolong/avbroot/pull/226
|
||||
[PR #227]: https://github.com/chenxiaolong/avbroot/pull/227
|
||||
[PR #228]: https://github.com/chenxiaolong/avbroot/pull/228
|
||||
[PR #229]: https://github.com/chenxiaolong/avbroot/pull/229
|
||||
[PR #230]: https://github.com/chenxiaolong/avbroot/pull/230
|
||||
[PR #231]: https://github.com/chenxiaolong/avbroot/pull/231
|
||||
[PR #232]: https://github.com/chenxiaolong/avbroot/pull/232
|
||||
[PR #233]: https://github.com/chenxiaolong/avbroot/pull/233
|
||||
[PR #234]: https://github.com/chenxiaolong/avbroot/pull/234
|
||||
[PR #237]: https://github.com/chenxiaolong/avbroot/pull/237
|
||||
[PR #240]: https://github.com/chenxiaolong/avbroot/pull/240
|
||||
[PR #241]: https://github.com/chenxiaolong/avbroot/pull/241
|
||||
[PR #243]: https://github.com/chenxiaolong/avbroot/pull/243
|
||||
[PR #244]: https://github.com/chenxiaolong/avbroot/pull/244
|
||||
[PR #245]: https://github.com/chenxiaolong/avbroot/pull/245
|
||||
[PR #246]: https://github.com/chenxiaolong/avbroot/pull/246
|
||||
[PR #247]: https://github.com/chenxiaolong/avbroot/pull/247
|
||||
[PR #251]: https://github.com/chenxiaolong/avbroot/pull/251
|
||||
[PR #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
|
||||
|
||||
Generated
+876
-1062
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,10 +1,10 @@
|
||||
[workspace]
|
||||
default-members = ["avbroot"]
|
||||
members = ["avbroot", "e2e", "xtask"]
|
||||
members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0"
|
||||
version = "3.8.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
+328
-13
@@ -6,10 +6,50 @@ Note that while avbroot maintains a stable command line interface for the patchi
|
||||
|
||||
## `avbroot avb`
|
||||
|
||||
### Unpacking an AVB image
|
||||
|
||||
```bash
|
||||
avbroot avb unpack -i <input AVB image>
|
||||
```
|
||||
|
||||
This subcommand unpacks the vbmeta header and footer into `avb.toml`. If a footer is present, then the corresponding raw partition image is extracted into `raw.img`. Root vbmeta images (eg. `vbmeta` and `vbmeta_vendor`) do not have footers, while appended vbmeta images (eg. `boot` and `system`) do.
|
||||
|
||||
The vbmeta descriptor digests are validated during unpacking. For dm-verity images, if the image is corrupt, avbroot will attempt to use the FEC data to repair the file. If there is unrepairable data corruption, the command will fail, though the corrupted `raw.img` will still be fully written. If, for whatever reason, a successful exit status of 0 is needed even for corrupted files, use `--ignore-invalid`.
|
||||
|
||||
### Packing an AVB image
|
||||
|
||||
```bash
|
||||
avbroot avb pack -o <output AVB image> [--key <AVB private key>]
|
||||
```
|
||||
|
||||
This subcommand packs a new AVB image from the `avb.toml` file and, for appended vbmeta images, the `raw.img` file.
|
||||
|
||||
* If the original image was signed and the new data is unmodified, then the original signature is used as-is. (This means just unpacking and packing an image will always result in a byte-for-byte identical file.)
|
||||
* If the original image was signed and the new data is modified, then the newly packed image will be signed with the `--key`.
|
||||
* If the original image was not signed, then the newly packed image is not signed.
|
||||
* 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
|
||||
|
||||
```bash
|
||||
avbroot avb repack -i <input AVB image> -o <output AVB image>
|
||||
```
|
||||
|
||||
This subcommand is equivalent to `avbroot avb unpack` followed by `avbroot avb pack`, except it doesn't need to write any intermediate files to disk.
|
||||
|
||||
This is useful for repairing a dm-verify image or for re-signing any image with a specific key.
|
||||
|
||||
### Showing vbmeta header and footer information
|
||||
|
||||
```bash
|
||||
avbroot avb dump -i <image>
|
||||
avbroot avb info -i <image>
|
||||
```
|
||||
|
||||
This subcommand shows all of the vbmeta header and footer fields. `vbmeta` partition images will only have a header, while partitions with actual data (eg. boot images) will have both a header and a footer.
|
||||
@@ -20,10 +60,22 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti
|
||||
avbroot avb verify -i <root vbmeta image> -p <public key>
|
||||
```
|
||||
|
||||
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hashtree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
|
||||
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
|
||||
|
||||
If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted.
|
||||
|
||||
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
|
||||
@@ -32,7 +84,7 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
|
||||
avbroot boot unpack -i <input boot image>
|
||||
```
|
||||
|
||||
This subcommand unpacks all of the components of the boot image into the current directory by default (see `--help`). The header fields are saved to `header.toml` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
|
||||
This subcommand unpacks all of the components of the boot image into the current directory by default (see `--help`). The header fields are saved to `boot.toml` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
|
||||
|
||||
### Packing a boot image
|
||||
|
||||
@@ -58,26 +110,289 @@ avbroot boot info -i <input boot image>
|
||||
|
||||
All of the `boot` subcommands show the boot image information. This specific subcommand just does it without performing any other operation. To show avbroot's internal representation of the information, pass in `-d`.
|
||||
|
||||
## `avbroot ramdisk`
|
||||
## `avbroot cpio`
|
||||
|
||||
### Dumping a cpio archive
|
||||
### Unpacking a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot ramdisk dump -i <cpio archive>
|
||||
avbroot cpio unpack -i <input cpio archive>
|
||||
```
|
||||
|
||||
This subcommand dumps all information about a cpio archive to stdout. This includes the compression format, all header fields (including the trailer entry), and all data. If an entry's data can be decoded as UTF-8, then it is printed out as text. Otherwise, the binary data is printed out `\x##`-encoded for non-ASCII bytes. The escape-encoded data is truncated to 512 bytes by default to avoid outputting too much data, but this behavior can be disabled with `--no-truncate`.
|
||||
This subcommand unpacks the cpio archive. The list of file entries and their metadata, like permissions, are written to `cpio.toml`. The contents of regular files are extracted to `cpio_tree/`. Other file types, like symlinks and block devices, are not extracted at all. Their information only exists in the TOML file.
|
||||
|
||||
The files inside the tree will have default permissions, ownership, and modification timestamps. This metadata exists only inside the TOML file in order to ensure that the behavior is the same across all platforms.
|
||||
|
||||
Both uncompressed archives and compressed archives (gzip or legacy lz4) are supported.
|
||||
|
||||
### Packing a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot cpio pack -o <output cpio archive>
|
||||
```
|
||||
|
||||
This subcommand packs a new cpio archive from the file entries listed in `cpio.toml` and the file contents in `cpio_tree/`. Note that **only entries listed in the TOML file are packed**. Extra files inside the tree are silently ignored.
|
||||
|
||||
The files are packed in the order listed in the TOML file. When packing ramdisks specifically, it's important to ensure that files are listed after their parent directories (which also **must** exist). Otherwise, the kernel will ignore them. As long as the file paths don't contain anything weird (eg. `a//b` or `a/./b`), sorting the entires with `--sort` should do the trick.
|
||||
|
||||
The new archive will be written in the same format (compressed or uncompressed) as the original archive.
|
||||
|
||||
### Repacking a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot ramdisk repack -i <input cpio archive> -o <output cpio archive>
|
||||
avbroot cpio repack -i <input cpio archive> -o <output cpio archive>
|
||||
```
|
||||
|
||||
This subcommand repacks a cpio archive, including recompression if needed. This is useful for roundtrip testing of avbroot's cpio parser and compression handling. The uncompressed output should be identical to the uncompressed input, except:
|
||||
This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot cpio pack`, except inode numbers will not be reassigned.
|
||||
|
||||
* files are sorted by name
|
||||
* inodes are reassigned, starting from 300000
|
||||
* there is no excess padding at the end of the file
|
||||
### Showing information about a cpio archive
|
||||
|
||||
The compressed output may differ from what other tools produce due to differences in compression levels and header metadata. avbroot avoids specifying header information where possible (eg. gzip timestamp) for reproducibility.
|
||||
```bash
|
||||
avbroot cpio info -i <input cpio archive>
|
||||
```
|
||||
|
||||
All of the `cpio` subcommands show details about all the entries in the archive. This specific subcommand just does it without performing any other operation.
|
||||
|
||||
## `avbroot fec`
|
||||
|
||||
This set of commands is for working with dm-verity FEC (forward error correction) data. The FEC data allows small errors in partition data to be corrected. This increases reliability of the system because when dm-verity encounters data that doesn't match the expected checksum, it will either trigger a kernel panic or reboot the system.
|
||||
|
||||
The same raw FEC data can be stored in several ways:
|
||||
|
||||
* cryptsetup's `veritysetup` does not use any file format at all. It must be told the FEC location and parameters using the `--fec-*` options.
|
||||
* AOSP's AVB 2.0 stores the FEC data inside the partition as `[Partition data][Hash tree][FEC data]`. The location and parameters are stored in the vbmeta hash tree descriptors.
|
||||
* AOSP's `fec` tool stores the FEC data in a standalone file with a header containing the FEC parameters.
|
||||
|
||||
The `avbroot fec` commands use AOSP's standalone FEC file format.
|
||||
|
||||
The FEC data is not generated from a sequential read of the input file, but rather from an interleaved read. If the input file's offsets are visualized as a 2D table:
|
||||
|
||||
```
|
||||
| 0 1 2 3 ... 4095 |
|
||||
| 4096 4097 4098 4099 ... 8191 |
|
||||
| 8192 8193 8194 8195 ... 12287 |
|
||||
| .... .... .... .... ... ..... |
|
||||
```
|
||||
|
||||
then the file access pattern can be thought of as being column-by-column instead of row-by-row.
|
||||
|
||||
Data correction happens at the codeword level. A Reed-Solomon codeword is 255 bytes where some portion is file data and the rest is parity data. AOSP and avbroot both default to 253 bytes of data and 2 bytes of parity information. Each column in the table represents the 253-byte data portion of the codeword. Larger files have more columns.
|
||||
|
||||
A contiguous sequence of corrupted data will span multiple columns. Since error correction happens at the column level, this interleaving increases the chances of recovery. For more details about the specifics, see the implementation in [`fec.rs`](./avbroot/src/format/fec.rs).
|
||||
|
||||
### Generating FEC data
|
||||
|
||||
```bash
|
||||
avbroot fec generate -i <input data file> -f <output FEC file>
|
||||
```
|
||||
|
||||
The default behavior is to use 2 bytes of parity information per 253 bytes of input data. Within each 253-byte column described above, this is sufficient for correcting a single corrupted byte in the column (`⌊parity / 2⌋` bytes in general).
|
||||
|
||||
The number of parity bytes (between 2 and 24, inclusive) can be configured using `--parity`.
|
||||
|
||||
### Updating FEC data
|
||||
|
||||
```bash
|
||||
avbroot fec update -i <input data file> -f <FEC file> [-r <start> <end>]...
|
||||
```
|
||||
|
||||
This will update the FEC data corresponding to the specified regions. This can be significantly faster than generating new FEC data from scratch for large files if the regions where data was modified are known.
|
||||
|
||||
### Verifying a file
|
||||
|
||||
```bash
|
||||
avbroot fec verify -i <input data file> -f <input FEC file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted bytes. This command runs significantly faster than `avbroot fec repair` and is useful if only detection of corrupted data is needed.
|
||||
|
||||
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, there can be false positives where the corrupted data is reported as being valid.
|
||||
|
||||
### Repairing a file
|
||||
|
||||
```bash
|
||||
avbroot fec repair -i <input/output data file> -f <input FEC file>
|
||||
```
|
||||
|
||||
This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected.
|
||||
|
||||
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data.
|
||||
|
||||
## `avbroot hash-tree`
|
||||
|
||||
This set of commands is for working with dm-verity hash tree data. They are not especially useful outside of debugging avbroot itself because the output format is custom. There is a custom header that sits in front of the standard dm-verity hash tree data.
|
||||
|
||||
| Offsets | Type | Description |
|
||||
|------------|--------|--------------------------------|
|
||||
| 0..16 | ASCII | `avbroot!hashtree` magic bytes |
|
||||
| 16..18 | U16LE | Version (currently 1) |
|
||||
| 18..26 | U64LE | Image size |
|
||||
| 26..30 | U32LE | Block size |
|
||||
| 30..46 | ASCII | Hash algorithm |
|
||||
| 46..48 | U16LE | Salt size |
|
||||
| 48..50 | U16LE | Root digest size |
|
||||
| 50..54 | U32LE | Hash tree size |
|
||||
| (Variable) | BINARY | Salt |
|
||||
| (Variable) | BINARY | Root digest |
|
||||
| (Variable) | BINARY | Hash tree |
|
||||
|
||||
For more information on the hash tree data, see the [Linux kernel documentation](https://docs.kernel.org/admin-guide/device-mapper/verity.html#hash-tree) or avbroot's implementation in [`hashtree.rs`](./avbroot/src/format/hashtree.rs).
|
||||
|
||||
### Generating hash tree
|
||||
|
||||
```bash
|
||||
avbroot hash-tree generate -i <input data file> -H <output hash tree file>
|
||||
```
|
||||
|
||||
The default behavior is to use a block size of 4096, the `sha256` algorithm, and an empty salt. These can be changed with the `-b`, `-a`, and `-s` options, respectively.
|
||||
|
||||
All parameters needed for verification are included in the hash tree file's header.
|
||||
|
||||
### Updating hash tree
|
||||
|
||||
```bash
|
||||
avbroot hash-tree update -i <input data file> -H <hash tree file> [-r <start> <end>]...
|
||||
```
|
||||
|
||||
This will update the hash tree data corresponding to the specified regions. This can be significantly faster than generating new hash tree data from scratch for large files if the regions where data was modified are known.
|
||||
|
||||
### Verifying a file
|
||||
|
||||
```bash
|
||||
avbroot hash-tree verify -i <input data file> -H <input hash tree file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
|
||||
|
||||
## `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,49 +1,117 @@
|
||||
# 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.
|
||||
(This page is also available in: [Russian (Русский)](./README.ru.md).)
|
||||
|
||||
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
|
||||
|
||||
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
|
||||
|
||||
**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:
|
||||
|
||||
* `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.
|
||||
|
||||
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 two patches to the boot images:
|
||||
avbroot applies the following patches to the partition images:
|
||||
|
||||
* Magisk is applied to the `boot` or `init_boot` image, depending on device, as if it were done from the Magisk app.
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
|
||||
|
||||
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded after the bootloader has been locked. It also prevents accidental flashing of the original OTA package while booted into recovery.
|
||||
* 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` 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
|
||||
|
||||
* The device must use (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. To check if a device uses this partitioning sceme, open the OTA zip file and check that:
|
||||
* **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_**.
|
||||
|
||||
* `payload.bin` exists
|
||||
* `META-INF/com/android/metadata.pb` exists
|
||||
* `META-INF/com/android/metadata` contains the line: `ota-type=AB`
|
||||
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
|
||||
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders support this, as well as most OnePlus devices. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
* 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:
|
||||
|
||||
* **Do not ever disable the `OEM unlocking` checkbox when using a locked bootloader with root.** This is critically important. With root access, it is possible to corrupt the running system, for example by zeroing out the boot partition. 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_**.
|
||||
* 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 unsigned or differently-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
|
||||
* 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.
|
||||
|
||||
* Performing a regular (unpatched) A/B OTA update. This can be blocked via a Magisk module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
|
||||
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.
|
||||
|
||||
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
|
||||
## Usage
|
||||
|
||||
1. Make sure the [caveats listed above](#warnings-and-caveats) are understood. It is possible to hard brick by doing the wrong thing!
|
||||
|
||||
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
|
||||
|
||||
avbroot is a standalone executable. It does not need to be installed and can be run from anywhere.
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
4. Patch the OTA zip. The base command is:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--input /path/to/ota.zip \
|
||||
--key-avb /path/to/avb.key \
|
||||
--key-ota /path/to/ota.key \
|
||||
--cert-ota /path/to/ota.crt \
|
||||
```
|
||||
|
||||
Add the following additional arguments to the end of the command depending on how you want to configure root access.
|
||||
|
||||
* To enable root access with Magisk:
|
||||
|
||||
```bash
|
||||
--magisk /path/to/magisk.apk \
|
||||
--magisk-preinit-device <name>
|
||||
```
|
||||
|
||||
If you don't know the Magisk preinit partition name, see the [Magisk preinit device section](#magisk-preinit-device) for steps on how to find it.
|
||||
|
||||
If you prefer to manually patch the boot image via the Magisk app instead of letting avbroot handle it, use the following arguments instead:
|
||||
|
||||
```bash
|
||||
--prepatched /path/to/magisk_patched-xxxxx_yyyyy.img
|
||||
```
|
||||
|
||||
* To enable root access with KernelSU:
|
||||
|
||||
```bash
|
||||
--prepatched /path/to/kernelsu/boot.img
|
||||
```
|
||||
|
||||
* To leave the OS unrooted:
|
||||
|
||||
```bash
|
||||
--rootless
|
||||
```
|
||||
|
||||
For more details on the options above, see the [advanced usage section](#advanced-usage).
|
||||
|
||||
If `--output` is not specified, then the output file is written to `<input>.patched`.
|
||||
|
||||
5. The patched OTA is ready to go! To flash it for the first time, follow the steps in the [initial setup section](#initial-setup). For updates, follow the steps in the [updates section](#updates).
|
||||
|
||||
## Generating Keys
|
||||
|
||||
avbroot signs a few components while patching an OTA zip:
|
||||
avbroot signs several components while patching an OTA zip:
|
||||
|
||||
* the root `vbmeta` image
|
||||
* the boot image `vbmeta` footers (if the original ones were signed)
|
||||
* the boot images
|
||||
* the vbmeta images
|
||||
* the OTA payload
|
||||
* the OTA zip itself
|
||||
|
||||
The boot-related components are signed with an AVB key and OTA-related components are signed with an OTA key. They can be the same RSA keypair, though the following steps show how to generate two separate keys.
|
||||
The first two components are signed with an AVB key and latter two components are signed with an OTA key. They can be the same key, though the following steps show how to generate two separate keys.
|
||||
|
||||
1. Generate the AVB and OTA signing keys:
|
||||
When patching OTAs for multiple devices, generating unique keys for each device is strongly recommended because it prevents an OTA for the wrong device being accidentally flashed.
|
||||
|
||||
1. Generate the AVB and OTA signing keys.
|
||||
|
||||
```bash
|
||||
avbroot key generate-key -o avb.key
|
||||
@@ -56,114 +124,173 @@ The boot-related components are signed with an AVB key and OTA-related component
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery for verifying OTA updates.
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
|
||||
|
||||
```bash
|
||||
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 X509 certificate (eg. 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.
|
||||
|
||||
## Usage
|
||||
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.
|
||||
|
||||
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
|
||||
## Initial setup
|
||||
|
||||
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
4. Patch the full OTA ZIP.
|
||||
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.
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--input /path/to/ota.zip \
|
||||
--privkey-avb /path/to/avb.key \
|
||||
--privkey-ota /path/to/ota.key \
|
||||
--cert-ota /path/to/ota.crt \
|
||||
--magisk /path/to/magisk.apk
|
||||
fastboot --version
|
||||
```
|
||||
|
||||
If `--output` is not specified, then the output file is written to `<input>.patched`.
|
||||
2. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
|
||||
|
||||
**NOTE:** If you are using Magisk version >=25211, you need to know the preinit partition name (`--magisk-preinit-device <name>`). For details, see the [Magisk preinit device section](#magisk-preinit-device).
|
||||
```bash
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
If you prefer to use an existing boot image patched by the Magisk app or you want to use KernelSU, see the [advanced usage section](#advanced-usage).
|
||||
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.
|
||||
|
||||
5. **[Initial setup only]** Unlock the bootloader. This will trigger a data wipe.
|
||||
|
||||
6. **[Initial setup only]** Extract the patched images from the patched OTA.
|
||||
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
|
||||
```
|
||||
|
||||
7. **[Initial setup only]** Flash the patched images and the AVB public key metadata. This sets up the custom root of trust. Future updates are done by simply sideloading patched OTA zips.
|
||||
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
|
||||
|
||||
5. Set the `ANDROID_PRODUCT_OUT` environment variable to the directory containing the extracted files.
|
||||
|
||||
For sh/bash/zsh (Linux, macOS, WSL):
|
||||
|
||||
```bash
|
||||
# Flash the boot images that were extracted
|
||||
for image in extracted/*.img; do
|
||||
partition=$(basename "${image}")
|
||||
partition=${partition%.img}
|
||||
export ANDROID_PRODUCT_OUT=extracted
|
||||
```
|
||||
|
||||
fastboot flash "${partition}" "${image}"
|
||||
done
|
||||
For PowerShell (Windows):
|
||||
|
||||
# Flash the AVB signing public key
|
||||
```powershell
|
||||
$env:ANDROID_PRODUCT_OUT = "extracted"
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
8. **[Initial setup only]** Run `dmesg | grep libfs_avb` as root to verify that AVB is working properly. A message similar to the following is expected:
|
||||
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:
|
||||
|
||||
```bash
|
||||
adb shell su -c 'dmesg | grep libfs_avb'
|
||||
```
|
||||
|
||||
If AVB is working properly, the following message should be printed out:
|
||||
|
||||
```bash
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
9. **[Initial setup only]** Lock the bootloader. This will trigger a data wipe again. **Do not uncheck `OEM unlocking`!**
|
||||
9. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
|
||||
|
||||
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using [avbroot's `oemunlockonboot` Magisk module](#oemunlockonboot-enable-oem-unlocking-on-every-boot) to automatically ensure OEM unlocking is enabled on every boot.
|
||||
```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.
|
||||
|
||||
10. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
|
||||
|
||||
## Updates
|
||||
|
||||
To update Android or Magisk:
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
|
||||
|
||||
1. Follow step 4 in [the previous section](#usage) to patch the new OTA (or an existing OTA with a newer Magisk APK).
|
||||
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.
|
||||
|
||||
2. Reboot to recovery mode. If stuck at a `No command` screen, press the volume up button once while holding down the power button.
|
||||
2. Follow the step in the [usage section](#usage) to patch the new OTA.
|
||||
|
||||
3. Sideload the patched OTA.
|
||||
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. Reboot.
|
||||
4. Sideload the patched OTA with `adb sideload`.
|
||||
|
||||
## avbroot Magisk modules
|
||||
5. That's it!
|
||||
|
||||
avbroot's Magisk modules can be found on the [releases page](https://github.com/chenxiaolong/avbroot/releases) or they can be built locally by running:
|
||||
## Reverting to stock firmware
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
```
|
||||
To stop using avbroot and revert to the stock firmware:
|
||||
|
||||
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
|
||||
1. Reboot into fastboot mode and unlock the bootloader. This will trigger a data wipe.
|
||||
|
||||
### `clearotacerts`: Blocking A/B OTA Updates
|
||||
2. Erase the custom AVB public key.
|
||||
|
||||
Unpatched OTA updates are already blocked in recovery because the original OTA certificate has been replaced with the custom certificate. To disable automatic OTAs while booted into Android, turn off `Automatic system updates` in Android's Developer Options.
|
||||
```bash
|
||||
fastboot erase avb_custom_key
|
||||
```
|
||||
|
||||
The `clearotacerts` module additionally makes A/B OTAs fail while booted into Android to prevent accidental manual updates. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
|
||||
3. Flash the stock firmware.
|
||||
|
||||
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
|
||||
4. That's it! There are no other remnants to clean up.
|
||||
|
||||
### `oemunlockonboot`: Enable OEM unlocking on every boot
|
||||
## OTA updates
|
||||
|
||||
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS's initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
|
||||
avbroot replaces `/system/etc/security/otacerts.zip` in both the system and recovery partitions with a new zip that contains the custom OTA signing certificate. This prevents an unpatched OTA from inadvertently being installed both when booted into Android and when sideloading from recovery.
|
||||
|
||||
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
|
||||
Disabling the system updater app is recommended to prevent it from even attempting to install an unpatched OTA. To do so:
|
||||
|
||||
* Stock OS: Turn off `Automatic system updates` in Android's Developer Options.
|
||||
* Custom OS: Disable the system updater app (or block its network access) from Settings -> Apps -> See all apps -> (three-dot menu) -> Show system -> (find updater app).
|
||||
|
||||
This is especially important for some custom OS's because their system updater app may get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails.
|
||||
|
||||
To self-host a custom OTA server, see [Custota](https://github.com/chenxiaolong/Custota).
|
||||
|
||||
## Repair mode
|
||||
|
||||
Some devices now ship with a Repair Mode feature that boots the system with a fresh `userdata` image so that repair technicians are able to run on-device diagnostics without needing the user's credentials to unlock the device.
|
||||
|
||||
When the device is rooted, it is unsafe to use Repair Mode. Unless you are using release builds of Magisk/KernelSU signed with your own keys, it's trivial for someone to just install the Magisk/KernelSU app while in repair mode to gain root access with no authentication.
|
||||
|
||||
To safely use Repair Mode:
|
||||
|
||||
1. Unroot the device by repatching the OTA with the `--rootless` option (instead of `--magisk` or `--prepatched`) and flashing it.
|
||||
|
||||
2. Turn on Repair Mode.
|
||||
|
||||
3. After receiving the repaired device, exit Repair Mode.
|
||||
|
||||
4. Flash the (rooted) patched OTA as normal.
|
||||
|
||||
Because the unrooting and rooting are done by flashing OTAs, the device's data will not be wiped.
|
||||
|
||||
## Magisk preinit device
|
||||
|
||||
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition's block device name to be specified via `--magisk-preinit-device <name>`. To find the partition name:
|
||||
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition to be explicitly specified via `--magisk-preinit-device <name>`. To find the partition name:
|
||||
|
||||
1. Extract the boot image from the original/unpatched OTA:
|
||||
|
||||
@@ -174,9 +301,9 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
--boot-only
|
||||
```
|
||||
|
||||
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device! The partition name will be incorrect if patched from Magisk on a different 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.
|
||||
|
||||
The Magisk app will include a line like the following in the output:
|
||||
The Magisk app will print out a line like the following in the output:
|
||||
|
||||
```
|
||||
- Pre-init storage partition device ID: <name>
|
||||
@@ -185,7 +312,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
Alternatively, avbroot can print out what Magisk detected by running:
|
||||
|
||||
```bash
|
||||
avbroot ota magisk-info \
|
||||
avbroot boot magisk-info \
|
||||
--image magisk_patched-*.img
|
||||
```
|
||||
|
||||
@@ -206,6 +333,8 @@ avbroot ota verify \
|
||||
--public-key-avb /path/to/avb_pkmd.bin
|
||||
```
|
||||
|
||||
This command works for any OTA, regardless if it's patched or unpatched.
|
||||
|
||||
If the `--cert-ota` and `--public-key-avb` options are omitted, then the signatures are only checked for validity, not that they are trusted.
|
||||
|
||||
## Tab completion
|
||||
@@ -248,15 +377,13 @@ Invoke-Expression (& avbroot completion -s powershell)
|
||||
|
||||
### Using a prepatched boot image
|
||||
|
||||
avbroot can replace the boot image with a prepatched image instead of applying the Magisk root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
|
||||
|
||||
For KernelSU, also pass in `--boot-partition @gki_kernel` for both the `patch` and `extract` commands. avbroot defaults to Magisk's semantics where the boot image containing the GKI ramdisk is needed, whereas KernelSU requires the boot image containing the GKI kernel. This only affects devices launching with Android 13, where the GKI kernel and ramdisk are in different partitions (`boot` vs. `init_boot`), but it is safe and recommended to always use this option for KernelSU.
|
||||
avbroot can replace the boot image with a prepatched image instead of applying the root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched Magisk boot image or a KernelSU boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
|
||||
|
||||
Note that avbroot will validate that the prepatched image is compatible with the original. If, for example, the header fields do not match or a boot image section is missing, then the patching process will abort. The checks are not foolproof, but should help protect against accidental use of the wrong boot image. To bypass a somewhat "safe" subset of the checks, use `--ignore-prepatched-compat`. To ignore all checks (strongly discouraged!), pass it in twice.
|
||||
|
||||
### Skipping root patches
|
||||
|
||||
avbroot can be used for just resigning 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.
|
||||
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.
|
||||
|
||||
### Replacing partitions
|
||||
|
||||
@@ -266,12 +393,18 @@ 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:
|
||||
|
||||
```
|
||||
ValueError: vbmeta flags disable AVB: 0x3
|
||||
Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
```
|
||||
|
||||
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
|
||||
@@ -280,7 +413,7 @@ To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
|
||||
|
||||
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
|
||||
|
||||
* Supply the passphrases via files:
|
||||
* Supply the passphrases via files.
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
@@ -323,6 +456,34 @@ avbroot ota extract \
|
||||
--all
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
## Building from source
|
||||
|
||||
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
|
||||
@@ -335,27 +496,21 @@ The output binary is written to `target/release/avbroot`.
|
||||
|
||||
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
|
||||
|
||||
By default, the build 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`.
|
||||
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.
|
||||
|
||||
```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
|
||||
|
||||
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
# avbroot
|
||||
|
||||
avbroot – это программа для модификации OTA-образов Android A/B-формата с целью получения root-прав при сохранении прохождения AVB (Android Verified Boot) с использованием кастомных (пользовательских) ключей подписи. Она совместима как с Magisk, так и с KernelSU. При необходимости можно просто переподписать OTA, без получения root-доступа.
|
||||
|
||||
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
|
||||
|
||||
**ПРИМЕЧАНИЕ:** avbroot 2.0 была переписана на Rust и больше не имеет в основе никакого кода AOSP, а CLI полностью обратно совместим. Тем не менее, старую реализацию на Python можно найти в одноименной ветке `python`.
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
|
||||
|
||||
* наличие файла `payload.bin` (обычно находится в корне архива)
|
||||
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
|
||||
|
||||
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это производится с помощью команды `fastboot flash avb_custom_key`.
|
||||
|
||||
Список девайсов, на которых проверялась совместимость с указанным выше функционалом, находится здесь: [#299.](https://github.com/chenxiaolong/avbroot/issues/299)
|
||||
|
||||
## Патчи
|
||||
|
||||
avbroot модифицирует следующие образы:
|
||||
|
||||
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа. В случае с Magisk, патч будет эквивалентен тому, что производится в самом приложении Magisk.
|
||||
|
||||
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
|
||||
|
||||
* `system` тоже модифицируется для замены сертификата проверки подписи OTA. Это не позволит системному приложению обновлений ОС установить оригинальный непропатченный OTA и дает возможность использовать сторонние приложения для установки пропатченных OTA.
|
||||
|
||||
## Предостережения
|
||||
|
||||
* **Всегда оставляйте опцию `Заводской разблокировки`** (или OEM unlocking в англ.) **включенной при наличии root-прав с заблокированным загрузчиком.** Это очень важно. Доступ к root-правам потенциально позволяет перезаписать загрузочный раздел из-под системы, будь то сделано случайно или намеренно, файлом, который не был подписан должным образом. В таком случае, система и режим Recovery больше не смогут загрузиться, а команда `fastboot flashing unlock` будет недоступна, потому что параметр Заводской разблокировки отключен. То есть, это приведет к **_хардбрику устройства_**.
|
||||
|
||||
Повторюсь: **_ВСЕГДА оставляйте `Заводскую разблокировку` включенной при наличии root-прав._**
|
||||
|
||||
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). К подобным операциям в том числе относятся:
|
||||
|
||||
* Метод `Прямой установки` для обновления Magisk. Magisk можно обновлять **только путем репатчинга OTA,** но не через его приложение.
|
||||
|
||||
* Функция `Удаление Magisk` в приложении 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 extract-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
|
||||
|
||||
```bash
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке 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 как при загрузке в Android, так и при сайдлоадинге через Recovery.
|
||||
|
||||
Рекомендуется отключить приложение обновлений системы, чтобы оно не пыталось установить непропатченные OTA:
|
||||
|
||||
* Стоковая прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
|
||||
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
|
||||
|
||||
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
|
||||
|
||||
Если вы хотите поднять собственный сервер для ОТА-обновлений, вам может быть интересно приложение [Custota.](https://github.com/chenxiaolong/Custota)
|
||||
|
||||
## Режим обслуживания
|
||||
|
||||
Некоторые устройства поставляются с режимом обслуживания, который загружает систему с чистым образом `userdata`, благодаря чему специалист по ремонту может проводить диагностику устройства, не имея доступа к пользовательским данным владельца.
|
||||
|
||||
Если на устройстве есть root-права, использовать этот режим небезопасно. Если у вас обычная сборка Magisk/KernelSU, не подписанная вашим собственным ключом, кто угодно может установить официальное приложение Magisk/KernelSU в режиме обслуживания и запросто получить root-права без какой-либо аутентификации.
|
||||
|
||||
Потому, чтобы безопасно использовать режим обслуживания:
|
||||
|
||||
1. Отключите root-доступ на устройстве, пропатчив OTA с аргументом `--rootless` (вместо `--magisk` или `--prepatched`) и прошив его.
|
||||
|
||||
2. Включите режим обслуживания.
|
||||
|
||||
3. Получив отремонтированное устройство обратно, выйдите из режима обслуживания.
|
||||
|
||||
4. Прошейте рутированный OTA в обычном режиме.
|
||||
|
||||
Поскольку удаление root-прав и повторное их получение выполняются путем перепрошивки OTA, данные устройства стёрты не будут.
|
||||
|
||||
## Предварительная инициализация устройства для Magisk
|
||||
|
||||
Magisk версии 25211 и новее требует наличие раздела, доступного для записи пользовательских правил SELinux, к которым необходимо обращаться на ранних этапах загрузки. Его можно определить только на реальном устройстве, поэтому avbroot требует указания точного названия с помощью аргумента `--magisk-preinit-device <имя>`. Чтобы получить имя раздела:
|
||||
|
||||
1. Извлеките загрузочный образ из оригинального, непропатченного OTA:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
```
|
||||
|
||||
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
|
||||
|
||||
Приложение Magisk выведет в лог строку, подобную следующей:
|
||||
|
||||
```
|
||||
- Pre-init storage partition device ID: <имя>
|
||||
```
|
||||
|
||||
Также и avbroot может вывести информацию о разделе, обнаруженном Magisk, для этого выполните команду:
|
||||
|
||||
```bash
|
||||
avbroot boot magisk-info \
|
||||
--image magisk_patched-*.img
|
||||
```
|
||||
|
||||
Имя раздела будет выведено как: `PREINITDEVICE=<имя>`.
|
||||
|
||||
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
|
||||
|
||||
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
|
||||
|
||||
## Проверка OTA
|
||||
|
||||
Чтобы проверить все подписи и хэши, связанные с установкой OTA и процессом загрузки AVB, выполните команду:
|
||||
|
||||
```bash
|
||||
avbroot ota verify \
|
||||
--input /путь/к/ota.zip \
|
||||
--cert-ota /путь/к/ota.crt \
|
||||
--public-key-avb /путь/к/avb_pkmd.bin
|
||||
```
|
||||
|
||||
Эта команда работает для любого OTA, независимо от того, пропатчено оно или нет.
|
||||
|
||||
Если опции `--cert-ota` и `--public-key-avb` не указаны, то подписи проверяются только на корректность, не проверяя, совпадают ли они внутри всех файлов.
|
||||
|
||||
## Подсказки через Tab
|
||||
|
||||
Поскольку avbroot имеет множество опций, будет удобно настроить подсказки с автозаполнением для используемой оболочки. Конфигурации генерируются в самом avbroot.
|
||||
|
||||
#### bash
|
||||
|
||||
Добавьте в `~/.bashrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s bash)"
|
||||
```
|
||||
|
||||
#### zsh
|
||||
|
||||
Добавьте в `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s zsh)"
|
||||
```
|
||||
|
||||
#### fish
|
||||
|
||||
Добавьте в `~/.config/fish/config.fish`:
|
||||
|
||||
```bash
|
||||
avbroot completion -s fish | source
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
Добавьте в загрузочный скрипт PowerShell (`profile.ps1`):
|
||||
|
||||
```powershell
|
||||
Invoke-Expression (& avbroot completion -s powershell)
|
||||
```
|
||||
|
||||
## Расширенное использование
|
||||
|
||||
### Использование заранее пропатченного boot.img
|
||||
|
||||
avbroot может подменить используемый загрузочный образ на заранее пропатченный (вместо того, чтобы самостоятельно применять патч). Это пригодится в случае, если у вас уже имеется пропатченный через приложение Magisk образ ядра или образ с поддержкой KernelSU. Для этого используйте аргумент `--prepatched <загрузочный образ>` вместо `--magisk <apk>`. То есть, указав `--prepatched`, avbroot пропустит применение патчинга Magisk'ом, но по-прежнему применит патч OTA-сертификата.
|
||||
|
||||
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
|
||||
|
||||
### Пропуск патчей для root-доступа
|
||||
|
||||
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
|
||||
|
||||
### Подмена разделов
|
||||
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
|
||||
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
|
||||
|
||||
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
|
||||
|
||||
### Очистка флагов vbmeta
|
||||
|
||||
Некоторые сборки Android-прошивок могут поставляться с образом `vbmeta`, в котором флаги установлены таким образом, что AVB фактически отключен. Если avbroot сталкивается с такими образом, процесс патчинга завершается ошибкой с сообщением следующего типа:
|
||||
|
||||
```
|
||||
Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
```
|
||||
|
||||
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
|
||||
|
||||
### Использование в неинтерактивном режиме
|
||||
|
||||
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
|
||||
|
||||
* Предоставить пароли через файлы:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--pass-avb-file /путь/к/avb.passphrase \
|
||||
--pass-ota-file /путь/к/ota.passphrase \
|
||||
<...>
|
||||
```
|
||||
|
||||
На Unix-подобных системах "файлы" могут быть каналами ("pipes"). В оболочках, поддерживающих подстановку процесса (bash, zsh и т. д.), пароль можно запросить с помощью команды (например, запрашивая у менеджера паролей).
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--pass-avb-file <(команда для запроса пароля AVB) \
|
||||
--pass-ota-file <(команда для запроса пароля OTA) \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Предоставить пароли через переменные среды. Это менее безопасно, поскольку любой процесс, запущенный от имени того же пользователя, может видеть значения переменных среды.
|
||||
|
||||
```bash
|
||||
export PASSPHRASE_AVB="пароль AVB"
|
||||
export PASSPHRASE_OTA="пароль OTA"
|
||||
|
||||
avbroot ota patch \
|
||||
--pass-avb-env-var PASSPHRASE_AVB \
|
||||
--pass-ota-env-var PASSPHRASE_OTA \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
|
||||
|
||||
### Извлечение всей OTA
|
||||
|
||||
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
```
|
||||
|
||||
### Режим записи 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 проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
Убедитесь, что у вас установлен [набор инструментов 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
-17
@@ -10,22 +10,32 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
base64 = "0.21.3"
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
byteorder = "1.4.3"
|
||||
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"] }
|
||||
const-oid = "0.9.5"
|
||||
crc32fast = "1.4.2"
|
||||
ctrlc = "3.4.0"
|
||||
dlv-list = "0.5.2"
|
||||
flate2 = "1.0.27"
|
||||
hex = "0.4.3"
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.3.0"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
miniz_oxide = "0.8.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
passterm = "2.0.3"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
quick-protobuf = "0.8.1"
|
||||
prost = "0.13.1"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
@@ -33,27 +43,27 @@ 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.16.20"
|
||||
rpassword = "7.2.0"
|
||||
ring = "0.17.0"
|
||||
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.19.14", features = ["serde"] }
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
xz2 = "0.1.7"
|
||||
zerocopy = "0.8.5"
|
||||
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
|
||||
# There are multiple upstream bugs that cause infinite loops in the Drop
|
||||
# implementation of write::BzDecoder. Unfortunately, the project is no longer
|
||||
# maintained, so we have to maintain our own fork with the necessary fixes.
|
||||
[dependencies.bzip2]
|
||||
git = "https://github.com/jongiddy/bzip2-rs"
|
||||
rev = "2aefcb4d3634de1df226c73d93f758d65228bb8c"
|
||||
git = "https://github.com/chenxiaolong/bzip2-rs"
|
||||
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
@@ -63,14 +73,19 @@ default-features = false
|
||||
features = ["deflate"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.158"
|
||||
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
# Disable the clap feature since it pulls in an ancient version of clap.
|
||||
pb-rs = { version = "0.10.0", default-features = false }
|
||||
constcat = "0.5.0"
|
||||
prost-build = "0.13.1"
|
||||
protox = "0.7.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["bzip2/static", "xz2/static"]
|
||||
static = ["bzip2/static", "liblzma/static"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
+61
-15
@@ -1,12 +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::{env, ffi::OsStr, fs, io, path::Path};
|
||||
|
||||
use pb_rs::{types::FileDescriptor, ConfigBuilder};
|
||||
|
||||
fn main() {
|
||||
let out_dir = Path::new(&env::var("OUT_DIR").unwrap()).join("protobuf");
|
||||
let in_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("protobuf");
|
||||
@@ -30,14 +26,64 @@ fn main() {
|
||||
|
||||
fs::create_dir_all(&out_dir).unwrap();
|
||||
|
||||
let config = ConfigBuilder::new(&protos, None, Some(&out_dir), &[in_dir])
|
||||
.unwrap()
|
||||
.dont_use_cow(true)
|
||||
// We're using this as a means to force quick-protobuf to use BTreeMap
|
||||
// instead of HashMap so that the serialized messages are reproducible.
|
||||
// https://github.com/tafia/quick-protobuf/issues/251
|
||||
.nostd(true)
|
||||
.build();
|
||||
let file_descriptors = protox::compile(&protos, [&in_dir]).unwrap();
|
||||
|
||||
FileDescriptor::run(&config).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();
|
||||
}
|
||||
|
||||
@@ -1,788 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Cursor, Read, Seek, Write},
|
||||
num::ParseIntError,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPrivateKey;
|
||||
use thiserror::Error;
|
||||
use x509_cert::Certificate;
|
||||
use xz2::{
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
format::{
|
||||
avb::{self, AlgorithmType, Descriptor},
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntryNew},
|
||||
},
|
||||
stream::{self, FromReader, HashingWriter, SectionReader, ToWriter},
|
||||
util::EscapedString,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Boot image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash descriptor found in vbmeta footer")]
|
||||
NoHashDescriptor,
|
||||
#[error("Found multiple hash descriptors in vbmeta footer")]
|
||||
MultipleHashDescriptors,
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
#[error("Failed to parse Magisk version from line: {0:?}")]
|
||||
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("XZ stream error")]
|
||||
XzStream(#[from] xz2::stream::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn load_ramdisk(data: &[u8]) -> Result<(Vec<CpioEntryNew>, CompressedFormat)> {
|
||||
let raw_reader = Cursor::new(data);
|
||||
let mut reader = CompressedReader::new(raw_reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false)?;
|
||||
|
||||
Ok((entries, reader.format()))
|
||||
}
|
||||
|
||||
fn save_ramdisk(entries: &[CpioEntryNew], format: CompressedFormat) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let mut writer = CompressedWriter::new(raw_writer, format)?;
|
||||
cpio::save(&mut writer, entries, false)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
pub trait BootImagePatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &Arc<AtomicBool>) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Root a boot image with Magisk.
|
||||
pub struct MagiskRootPatcher {
|
||||
apk_path: PathBuf,
|
||||
version: u32,
|
||||
preinit_device: Option<String>,
|
||||
random_seed: u64,
|
||||
}
|
||||
|
||||
impl MagiskRootPatcher {
|
||||
// - Versions <25102 are not supported because they're missing commit
|
||||
// 1f8c063dc64806c4f7320ed66c785ff7bc116383, which would leave devices
|
||||
// that use Android 13 GKIs unable to boot into recovery
|
||||
// - Versions 25207 through 25210 are not supported because they used the
|
||||
// RULESDEVICE config option, which stored the writable block device as an
|
||||
// rdev major/minor pair, which was not consistent across reboots and was
|
||||
// replaced by PREINITDEVICE
|
||||
const VERS_SUPPORTED: &[Range<u32>] = &[25102..25207, 25211..26300];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
|
||||
pub fn new(
|
||||
path: &Path,
|
||||
preinit_device: Option<&str>,
|
||||
random_seed: Option<u64>,
|
||||
ignore_compatibility: bool,
|
||||
warning_fn: impl Fn(&str) + Send + 'static,
|
||||
) -> Result<Self> {
|
||||
let version = Self::get_version(path)?;
|
||||
|
||||
if !Self::VERS_SUPPORTED.iter().any(|v| v.contains(&version)) {
|
||||
let msg = format!(
|
||||
"Unsupported Magisk version {} (supported: {:?})",
|
||||
version,
|
||||
Self::VERS_SUPPORTED,
|
||||
);
|
||||
|
||||
if ignore_compatibility {
|
||||
warning_fn(&msg);
|
||||
} else {
|
||||
return Err(Error::Validation(msg));
|
||||
}
|
||||
}
|
||||
|
||||
if preinit_device.is_none() && Self::VER_PREINIT_DEVICE.contains(&version) {
|
||||
let msg = format!(
|
||||
"Magisk version {} ({:?}) requires a preinit device to be specified",
|
||||
version,
|
||||
Self::VER_PREINIT_DEVICE,
|
||||
);
|
||||
|
||||
if ignore_compatibility {
|
||||
warning_fn(&msg);
|
||||
} else {
|
||||
return Err(Error::Validation(msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
apk_path: path.to_owned(),
|
||||
version,
|
||||
preinit_device: preinit_device.map(|d| d.to_owned()),
|
||||
// Use a hardcoded random seed by default to ensure byte-for-byte
|
||||
// reproducibility.
|
||||
random_seed: random_seed.unwrap_or(0xfedcba9876543210),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_version(path: &Path) -> Result<u32> {
|
||||
let reader = File::open(path)?;
|
||||
let reader = BufReader::new(reader);
|
||||
let mut zip = ZipArchive::new(reader)?;
|
||||
let entry = zip.by_name("assets/util_functions.sh")?;
|
||||
let mut entry = BufReader::new(entry);
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = entry.read_line(&mut line)?;
|
||||
if n == 0 {
|
||||
return Err(Error::FindMagiskVersion(path.to_owned()));
|
||||
}
|
||||
|
||||
if let Some(suffix) = line.trim_end().strip_prefix("MAGISK_VER_CODE=") {
|
||||
let version = suffix
|
||||
.parse()
|
||||
.map_err(|e| Error::ParseMagiskVersion(suffix.to_owned(), e))?;
|
||||
return Ok(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare old and new ramdisk entry lists, creating the Magisk `.backup/`
|
||||
/// directory structure. `.backup/.rmlist` will contain a sorted list of
|
||||
/// NULL-terminated strings, listing which files were newly added or
|
||||
/// changed. The old entries for changed files will be added to the new
|
||||
/// entries as `.backup/<path>`.
|
||||
///
|
||||
/// Both lists and entries within the lists may be mutated.
|
||||
fn apply_magisk_backup(old_entries: &mut [CpioEntryNew], new_entries: &mut Vec<CpioEntryNew>) {
|
||||
cpio::sort(old_entries);
|
||||
cpio::sort(new_entries);
|
||||
|
||||
let mut rm_list = vec![];
|
||||
let mut to_back_up = vec![];
|
||||
|
||||
let mut old_iter = old_entries.iter().peekable();
|
||||
let mut new_iter = new_entries.iter().peekable();
|
||||
|
||||
loop {
|
||||
match (old_iter.peek(), new_iter.peek()) {
|
||||
(Some(&old), Some(&new)) => match old.name.cmp(&new.name) {
|
||||
Ordering::Less => {
|
||||
to_back_up.push(old);
|
||||
old_iter.next();
|
||||
}
|
||||
Ordering::Equal => {
|
||||
if old.content != new.content {
|
||||
to_back_up.push(old);
|
||||
}
|
||||
old_iter.next();
|
||||
new_iter.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
rm_list.extend(&new.name);
|
||||
rm_list.push(b'\0');
|
||||
new_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(old), None) => {
|
||||
to_back_up.push(old);
|
||||
old_iter.next();
|
||||
}
|
||||
(None, Some(new)) => {
|
||||
rm_list.extend(&new.name);
|
||||
rm_list.push(b'\0');
|
||||
new_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Intentially using 000 permissions to match Magisk.
|
||||
new_entries.push(CpioEntryNew::new_directory(b".backup"));
|
||||
|
||||
for old_entry in to_back_up {
|
||||
let mut new_entry = old_entry.clone();
|
||||
new_entry.name = b".backup/".to_vec();
|
||||
new_entry.name.extend(&old_entry.name);
|
||||
new_entries.push(new_entry);
|
||||
}
|
||||
|
||||
{
|
||||
// Intentially using 000 permissions to match Magisk.
|
||||
let mut entry = CpioEntryNew::new_file(b".backup/.rmlist");
|
||||
entry.content = rm_list;
|
||||
new_entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for MagiskRootPatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
let zip_reader = File::open(&self.apk_path)?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
|
||||
|
||||
// Load the first ramdisk. If it doesn't exist, we have to generate one
|
||||
// from scratch.
|
||||
let ramdisk = match boot_image {
|
||||
BootImage::V0Through2(b) => Some(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => Some(&b.ramdisk),
|
||||
BootImage::VendorV3Through4(b) => b.ramdisks.first(),
|
||||
};
|
||||
let (mut entries, ramdisk_format) = match ramdisk {
|
||||
Some(r) if !r.is_empty() => load_ramdisk(r)?,
|
||||
_ => (vec![], CompressedFormat::Lz4Legacy),
|
||||
};
|
||||
|
||||
let mut old_entries = entries.clone();
|
||||
|
||||
// Create the Magisk directory structure.
|
||||
for (path, perms) in [
|
||||
(b"overlay.d".as_slice(), 0o750),
|
||||
(b"overlay.d/sbin".as_slice(), 0o750),
|
||||
] {
|
||||
let mut entry = CpioEntryNew::new_directory(path);
|
||||
entry.mode |= perms;
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
// Delete the original init.
|
||||
entries.retain(|e| e.name != b"init");
|
||||
|
||||
// Add magiskinit.
|
||||
{
|
||||
let mut zip_entry = zip.by_name("lib/arm64-v8a/libmagiskinit.so")?;
|
||||
let mut data = vec![];
|
||||
zip_entry.read_to_end(&mut data)?;
|
||||
|
||||
let mut entry = CpioEntryNew::new_file(b"init");
|
||||
entry.mode |= 0o750;
|
||||
entry.content = data;
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
// Add xz-compressed magisk32 and magisk64.
|
||||
let mut xz_files = HashMap::<&str, &[u8]>::new();
|
||||
xz_files.insert(
|
||||
"lib/armeabi-v7a/libmagisk32.so",
|
||||
b"overlay.d/sbin/magisk32.xz",
|
||||
);
|
||||
xz_files.insert(
|
||||
"lib/arm64-v8a/libmagisk64.so",
|
||||
b"overlay.d/sbin/magisk64.xz",
|
||||
);
|
||||
|
||||
// Add stub apk, which only exists after Magisk commit
|
||||
// ad0e6511e11ebec65aa9b5b916e1397342850319.
|
||||
if zip.file_names().any(|n| n == "assets/stub.apk") {
|
||||
xz_files.insert("assets/stub.apk", b"overlay.d/sbin/stub.xz");
|
||||
}
|
||||
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip.by_name(source)?;
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32)?;
|
||||
let mut writer = XzEncoder::new_stream(raw_writer, stream);
|
||||
|
||||
stream::copy(reader, &mut writer, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let mut entry = CpioEntryNew::new_file(target);
|
||||
entry.mode |= 0o644;
|
||||
entry.content = raw_writer.into_inner();
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
// Create Magisk .backup directory structure.
|
||||
Self::apply_magisk_backup(&mut old_entries, &mut entries);
|
||||
|
||||
// Create Magisk config.
|
||||
let mut magisk_config = String::new();
|
||||
magisk_config.push_str("KEEPVERITY=true\n");
|
||||
magisk_config.push_str("KEEPFORCEENCRYPT=true\n");
|
||||
magisk_config.push_str("PATCHVBMETAFLAG=false\n");
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
// Magisk normally saves the original SHA1 digest in its config file. It
|
||||
// uses this to find the original image in /data/magisk_backup_<sha1> to
|
||||
// restore the stock boot image for uninstallation purposes. This is a
|
||||
// feature we cannot ever use, so just use a dummy value.
|
||||
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));
|
||||
}
|
||||
|
||||
{
|
||||
// Intentially using 000 permissions to match Magisk.
|
||||
let mut entry = CpioEntryNew::new_file(b".backup/.magisk");
|
||||
entry.content = magisk_config.into_bytes();
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
// Repack ramdisk.
|
||||
cpio::sort(&mut entries);
|
||||
cpio::reassign_inodes(&mut entries);
|
||||
let new_ramdisk = save_ramdisk(&entries, ramdisk_format)?;
|
||||
|
||||
match boot_image {
|
||||
BootImage::V0Through2(b) => b.ramdisk = new_ramdisk,
|
||||
BootImage::V3Through4(b) => b.ramdisk = new_ramdisk,
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
if b.ramdisks.is_empty() {
|
||||
b.ramdisks.push(new_ramdisk);
|
||||
|
||||
if let Some(v4) = &mut b.v4_extra {
|
||||
v4.ramdisk_metas.push(RamdiskMeta {
|
||||
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_NONE,
|
||||
ramdisk_name: String::new(),
|
||||
board_id: Default::default(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
b.ramdisks[0] = new_ramdisk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the OTA certificates in the vendor_boot/recovery image with the
|
||||
/// custom OTA signing certificate.
|
||||
pub struct OtaCertPatcher {
|
||||
cert: Certificate,
|
||||
}
|
||||
|
||||
impl OtaCertPatcher {
|
||||
const OTACERTS_PATH: &[u8] = b"system/etc/security/otacerts.zip";
|
||||
|
||||
pub fn new(cert: Certificate) -> Self {
|
||||
Self { cert }
|
||||
}
|
||||
|
||||
pub fn get_certificates(boot_image: &BootImage) -> Result<Vec<Certificate>> {
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
match boot_image {
|
||||
BootImage::V0Through2(b) => ramdisks.push(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => ramdisks.push(&b.ramdisk),
|
||||
BootImage::VendorV3Through4(b) => ramdisks.extend(b.ramdisks.iter()),
|
||||
}
|
||||
|
||||
let mut certificates = vec![];
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
let (entries, _) = load_ramdisk(ramdisk)?;
|
||||
let Some(entry) = entries.iter().find(|e| e.name == Self::OTACERTS_PATH) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&entry.content))?;
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip.by_index(index)?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let certificate = crypto::read_pem_cert(zip_entry)?;
|
||||
certificates.push(certificate);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(certificates)
|
||||
}
|
||||
|
||||
fn patch_ramdisk(&self, data: &mut Vec<u8>) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(data)?;
|
||||
let Some(entry) = entries.iter_mut().find(|e| e.name == Self::OTACERTS_PATH) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
// Create a new otacerts archive. The old certs are ignored since
|
||||
// flashing a stock OTA will render the device unbootable.
|
||||
{
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &self.cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
entry.content = raw_writer.into_inner();
|
||||
}
|
||||
|
||||
// Repack ramdisk.
|
||||
*data = save_ramdisk(&entries, ramdisk_format)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for OtaCertPatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
let patched_any = match boot_image {
|
||||
BootImage::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk)?,
|
||||
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk)?,
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
let mut patched = false;
|
||||
|
||||
for ramdisk in &mut b.ramdisks {
|
||||
if self.patch_ramdisk(ramdisk)? {
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patched
|
||||
}
|
||||
};
|
||||
|
||||
// Fail hard if otacerts does not exist. We don't want to lock the user
|
||||
// out of future updates if the OTA certificate mechanism has changed.
|
||||
if !patched_any {
|
||||
return Err(Error::Validation(format!(
|
||||
"No ramdisk contains {}",
|
||||
EscapedString::new(Self::OTACERTS_PATH),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// identical and the set of included sections (eg. kernel, dtb) are the same.
|
||||
/// The only exception is the number of ramdisk sections, which is allowed to be
|
||||
/// higher than the original image.
|
||||
pub struct PrepatchedImagePatcher {
|
||||
prepatched: PathBuf,
|
||||
fatal_level: u8,
|
||||
warning_fn: Box<dyn Fn(&str) + Send>,
|
||||
}
|
||||
|
||||
impl PrepatchedImagePatcher {
|
||||
const MIN_LEVEL: u8 = 0;
|
||||
const MAX_LEVEL: u8 = 2;
|
||||
|
||||
// We compile without Unicode support so we have to use [0-9] instead of \d.
|
||||
const VERSION_REGEX: &str = r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
|
||||
|
||||
pub fn new(
|
||||
prepatched: &Path,
|
||||
fatal_level: u8,
|
||||
warning_fn: impl Fn(&str) + Send + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
prepatched: prepatched.to_owned(),
|
||||
fatal_level,
|
||||
warning_fn: Box::new(warning_fn),
|
||||
}
|
||||
}
|
||||
|
||||
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 regex = Regex::new(Self::VERSION_REGEX).unwrap();
|
||||
let Some(captures) = regex.captures(&decompressed) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let kmi_version = captures
|
||||
.iter()
|
||||
// Capture #0 is the entire match.
|
||||
.skip(1)
|
||||
.flatten()
|
||||
.map(|c| c.as_bytes())
|
||||
// Our regex only matches ASCII bytes.
|
||||
.map(|c| std::str::from_utf8(c).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
|
||||
Ok(Some(kmi_version))
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for PrepatchedImagePatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
let prepatched_image = {
|
||||
let raw_reader = File::open(&self.prepatched)?;
|
||||
BootImage::from_reader(BufReader::new(raw_reader))?
|
||||
};
|
||||
|
||||
// Level 0: Warnings that don't affect booting
|
||||
// Level 1: Warnings that may affect booting
|
||||
// Level 2: Warnings that are very likely to affect booting
|
||||
let mut issues = [vec![], vec![], vec![]];
|
||||
|
||||
macro_rules! check {
|
||||
($level:literal, $old:expr, $new:expr $(,)?) => {
|
||||
let old_val = $old;
|
||||
let new_val = $new;
|
||||
|
||||
if old_val != new_val {
|
||||
issues[$level].push(format!(
|
||||
"Field differs: {} ({:?}) -> {} ({:?})",
|
||||
stringify!($old),
|
||||
old_val,
|
||||
stringify!($new),
|
||||
new_val,
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let old_kernel;
|
||||
let new_kernel;
|
||||
|
||||
match (&boot_image, &prepatched_image) {
|
||||
(BootImage::V0Through2(old), BootImage::V0Through2(new)) => {
|
||||
check!(2, old.header_version(), new.header_version());
|
||||
check!(2, old.kernel_addr, new.kernel_addr);
|
||||
check!(2, old.ramdisk_addr, new.ramdisk_addr);
|
||||
check!(2, old.second_addr, new.second_addr);
|
||||
check!(2, old.tags_addr, new.tags_addr);
|
||||
check!(2, old.page_size, new.page_size);
|
||||
check!(0, old.os_version, new.os_version);
|
||||
check!(0, &old.name, &new.name);
|
||||
check!(1, &old.cmdline, &new.cmdline);
|
||||
check!(0, &old.id, &new.id);
|
||||
check!(1, &old.extra_cmdline, &new.extra_cmdline);
|
||||
check!(2, old.kernel.is_empty(), new.kernel.is_empty());
|
||||
check!(2, old.second.is_empty(), new.second.is_empty());
|
||||
|
||||
if let (Some(old_v1), Some(new_v1)) = (&old.v1_extra, &new.v1_extra) {
|
||||
check!(2, old_v1.recovery_dtbo_offset, new_v1.recovery_dtbo_offset);
|
||||
check!(
|
||||
2,
|
||||
old_v1.recovery_dtbo.is_empty(),
|
||||
new_v1.recovery_dtbo.is_empty(),
|
||||
);
|
||||
}
|
||||
|
||||
if let (Some(old_v2), Some(new_v2)) = (&old.v2_extra, &new.v2_extra) {
|
||||
check!(2, old_v2.dtb_addr, new_v2.dtb_addr);
|
||||
check!(2, old_v2.dtb.is_empty(), new_v2.dtb.is_empty());
|
||||
}
|
||||
|
||||
// We allow adding a ramdisk.
|
||||
if !old.ramdisk.is_empty() || new.ramdisk.is_empty() {
|
||||
check!(2, old.ramdisk.is_empty(), new.ramdisk.is_empty());
|
||||
}
|
||||
|
||||
old_kernel = if old.kernel.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&old.kernel)
|
||||
};
|
||||
new_kernel = if new.kernel.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&new.kernel)
|
||||
};
|
||||
}
|
||||
(BootImage::V3Through4(old), BootImage::V3Through4(new)) => {
|
||||
check!(2, old.header_version(), new.header_version());
|
||||
check!(0, old.os_version, new.os_version);
|
||||
check!(0, old.reserved, new.reserved);
|
||||
check!(1, &old.cmdline, &new.cmdline);
|
||||
check!(2, old.kernel.is_empty(), new.kernel.is_empty());
|
||||
|
||||
// We allow adding a ramdisk.
|
||||
if !old.ramdisk.is_empty() || new.ramdisk.is_empty() {
|
||||
check!(2, old.ramdisk.is_empty(), new.ramdisk.is_empty());
|
||||
}
|
||||
|
||||
old_kernel = if old.kernel.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&old.kernel)
|
||||
};
|
||||
new_kernel = if new.kernel.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&new.kernel)
|
||||
};
|
||||
}
|
||||
(BootImage::VendorV3Through4(old), BootImage::VendorV3Through4(new)) => {
|
||||
check!(2, old.page_size, new.page_size);
|
||||
check!(2, old.kernel_addr, new.kernel_addr);
|
||||
check!(2, old.ramdisk_addr, new.ramdisk_addr);
|
||||
check!(1, &old.cmdline, &new.cmdline);
|
||||
check!(2, old.tags_addr, new.tags_addr);
|
||||
check!(0, &old.name, &new.name);
|
||||
check!(2, old.dtb.is_empty(), new.dtb.is_empty());
|
||||
check!(2, old.dtb_addr, new.dtb_addr);
|
||||
check!(2, old.ramdisks.len(), new.ramdisks.len());
|
||||
|
||||
if let (Some(old_v4), Some(new_v4)) = (&old.v4_extra, &new.v4_extra) {
|
||||
check!(2, &old_v4.ramdisk_metas, &new_v4.ramdisk_metas);
|
||||
check!(2, &old_v4.bootconfig, &new_v4.bootconfig);
|
||||
}
|
||||
|
||||
old_kernel = None;
|
||||
new_kernel = None;
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::Validation(
|
||||
"Boot image and prepatched image are different boot image types".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(old), Some(new)) = (old_kernel, new_kernel) {
|
||||
let old_kmi_version = Self::get_kmi_version(old)?;
|
||||
let new_kmi_version = Self::get_kmi_version(new)?;
|
||||
|
||||
check!(2, old_kmi_version, new_kmi_version);
|
||||
}
|
||||
|
||||
let mut warnings = vec![];
|
||||
let mut errors = vec![];
|
||||
|
||||
for level in Self::MIN_LEVEL..self.fatal_level {
|
||||
warnings.extend(&issues[level as usize]);
|
||||
}
|
||||
for level in self.fatal_level..=Self::MAX_LEVEL {
|
||||
errors.extend(&issues[level as usize]);
|
||||
}
|
||||
|
||||
if !warnings.is_empty() {
|
||||
let mut msg =
|
||||
"The prepatched boot image may not be compatible with the original:".to_owned();
|
||||
for warning in warnings {
|
||||
msg.push_str("\n- ");
|
||||
msg.push_str(warning);
|
||||
}
|
||||
|
||||
(self.warning_fn)(&msg);
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
let mut msg =
|
||||
"The prepatched boot image is not compatible with the original:".to_owned();
|
||||
for error in errors {
|
||||
msg.push_str("\n- ");
|
||||
msg.push_str(error);
|
||||
}
|
||||
|
||||
return Err(Error::Validation(msg));
|
||||
}
|
||||
|
||||
*boot_image = prepatched_image;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Run each patcher against the boot image with the vbmeta footer stripped off
|
||||
/// and then re-sign the image.
|
||||
pub fn patch_boot(
|
||||
mut reader: impl Read + Seek,
|
||||
writer: impl Write + Seek,
|
||||
key: &RsaPrivateKey,
|
||||
patchers: &[Box<dyn BootImagePatcher + Send>],
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
) -> Result<()> {
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
|
||||
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
|
||||
let mut boot_image = BootImage::from_reader(section_reader)?;
|
||||
|
||||
for patcher in patchers {
|
||||
patcher.patch(&mut boot_image, cancel_signal)?;
|
||||
}
|
||||
|
||||
let mut descriptor_iter = header.descriptors.iter_mut().filter_map(|d| {
|
||||
if let Descriptor::Hash(h) = d {
|
||||
Some(h)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let Some(descriptor) = descriptor_iter.next() 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);
|
||||
boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
|
||||
header.algorithm_type = AlgorithmType::Sha256Rsa4096;
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
descriptor.hash_algorithm = "sha256".to_owned();
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if descriptor_iter.next().is_some() {
|
||||
return Err(Error::MultipleHashDescriptors);
|
||||
}
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(writer, &header, &footer, image_size)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+101
-10
@@ -1,14 +1,19 @@
|
||||
/*
|
||||
* 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::sync::{atomic::AtomicBool, Arc};
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, IsTerminal},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, key, ota, ramdisk};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -16,9 +21,14 @@ pub enum Command {
|
||||
Avb(avb::AvbCli),
|
||||
Boot(boot::BootCli),
|
||||
Completion(completion::CompletionCli),
|
||||
Cpio(cpio::CpioCli),
|
||||
Fec(fec::FecCli),
|
||||
HashTree(hashtree::HashTreeCli),
|
||||
Key(key::KeyCli),
|
||||
Lp(lp::LpCli),
|
||||
Ota(ota::OtaCli),
|
||||
Ramdisk(ramdisk::RamdiskCli),
|
||||
Payload(payload::PayloadCli),
|
||||
Sparse(sparse::SparseCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
@@ -27,22 +37,103 @@ pub enum Command {
|
||||
MagiskInfo(boot::MagiskInfoCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum LogFormat {
|
||||
Short,
|
||||
Medium,
|
||||
Long,
|
||||
}
|
||||
|
||||
impl Default for LogFormat {
|
||||
fn default() -> Self {
|
||||
Self::Short
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
|
||||
pub log_level: Level,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
|
||||
pub fn main(cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ShortUptime {
|
||||
epoch: Instant,
|
||||
}
|
||||
|
||||
impl Default for ShortUptime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
epoch: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatTime for ShortUptime {
|
||||
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
|
||||
let e = self.epoch.elapsed();
|
||||
write!(w, "{:3}.{:03}s", e.as_secs(), e.subsec_millis())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logging(log_level: Level, log_format: LogFormat) {
|
||||
let builder = tracing_subscriber::fmt()
|
||||
.with_writer(io::stderr)
|
||||
.with_ansi(io::stderr().is_terminal())
|
||||
.with_max_level(log_level);
|
||||
|
||||
match log_format {
|
||||
LogFormat::Short => {
|
||||
let format = tracing_subscriber::fmt::format()
|
||||
.with_timer(ShortUptime::default())
|
||||
.with_target(false);
|
||||
|
||||
builder.event_format(format).init();
|
||||
}
|
||||
LogFormat::Medium => {
|
||||
builder.with_timer(ShortUptime::default()).init();
|
||||
}
|
||||
LogFormat::Long => {
|
||||
builder.pretty().init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
init_logging(cli.log_level, cli.log_format);
|
||||
logging_initialized.store(true, Ordering::SeqCst);
|
||||
|
||||
debug!(?cli);
|
||||
|
||||
match cli.command {
|
||||
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
|
||||
Command::Boot(c) => boot::boot_main(&c),
|
||||
Command::Completion(c) => completion::completion_main(&c),
|
||||
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
|
||||
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
|
||||
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Lp(c) => lp::lp_main(&c, cancel_signal),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
Command::Ramdisk(c) => ramdisk::ramdisk_main(&c),
|
||||
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),
|
||||
|
||||
+936
-92
File diff suppressed because it is too large
Load Diff
+24
-20
@@ -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,11 +7,11 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio},
|
||||
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio::CpioReader},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
|
||||
@@ -33,7 +31,9 @@ fn write_image(path: &Path, image: &BootImage) -> Result<()> {
|
||||
image
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write boot image: {path:?}"))?;
|
||||
writer.flush()?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush boot image: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn read_avb_header_if_exists(path: &Path) -> Result<Option<Header>> {
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?,
|
||||
};
|
||||
let header = Header::from_reader(BufReader::new(file))
|
||||
.with_context(|| anyhow!("Failed to read vbmeta header: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to read vbmeta header: {path:?}"))?;
|
||||
|
||||
Ok(Some(header))
|
||||
}
|
||||
@@ -106,7 +106,7 @@ fn write_text_if_not_empty(path: &Path, text: &str) -> Result<()> {
|
||||
|
||||
fn write_avb_header(path: &Path, header: &Header) -> Result<()> {
|
||||
let file =
|
||||
File::create(path).with_context(|| anyhow!("Failed to open for writing: {path:?}"))?;
|
||||
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
header.to_writer(BufWriter::new(file))?;
|
||||
|
||||
Ok(())
|
||||
@@ -272,9 +272,9 @@ fn info_subcommand(boot_cli: &BootCli, cli: &InfoCli) -> Result<()> {
|
||||
|
||||
pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
|
||||
let raw_reader = File::open(&cli.image)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {:?}", cli.image))?;
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.image))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))
|
||||
.with_context(|| anyhow!("Failed to load boot image: {:?}", cli.image))?;
|
||||
.with_context(|| format!("Failed to load boot image: {:?}", cli.image))?;
|
||||
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
@@ -297,13 +297,17 @@ pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
|
||||
for (i, ramdisk) in ramdisks.iter().enumerate() {
|
||||
let reader = Cursor::new(ramdisk);
|
||||
let reader = CompressedReader::new(reader, true)
|
||||
.with_context(|| anyhow!("Failed to load ramdisk #{i}"))?;
|
||||
let entries = cpio::load(reader, false)
|
||||
.with_context(|| anyhow!("Failed to load ramdisk #{i} cpio"))?;
|
||||
.with_context(|| format!("Failed to load ramdisk #{i}"))?;
|
||||
let mut cpio_reader = CpioReader::new(reader, false);
|
||||
|
||||
if let Some(e) = entries.iter().find(|e| e.name == b".backup/.magisk") {
|
||||
io::stdout().write_all(&e.content)?;
|
||||
return Ok(());
|
||||
while let Some(entry) = cpio_reader
|
||||
.next_entry()
|
||||
.with_context(|| format!("Failed to read ramdisk #{i} cpio entry"))?
|
||||
{
|
||||
if entry.path == b".backup/.magisk" {
|
||||
io::copy(&mut cpio_reader, &mut io::stdout())?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +332,7 @@ struct UnpackCli {
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output header TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "header.toml")]
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
|
||||
output_header: PathBuf,
|
||||
|
||||
/// Path to output kernel image.
|
||||
@@ -388,7 +392,7 @@ struct PackCli {
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input header TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "header.toml")]
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
|
||||
input_header: PathBuf,
|
||||
|
||||
/// Path to input kernel image.
|
||||
@@ -477,7 +481,7 @@ enum BootCommand {
|
||||
MagiskInfo(MagiskInfoCli),
|
||||
}
|
||||
|
||||
/// Pack or unpack boot images.
|
||||
/// Pack, unpack, and inspect boot images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct BootCli {
|
||||
#[command(subcommand)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Seek},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Parser, Subcommand};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
compression::{CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
|
||||
},
|
||||
stream, util,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
struct CpioInfo {
|
||||
format: CompressedFormat,
|
||||
entries: Vec<CpioEntry>,
|
||||
}
|
||||
|
||||
fn open_reader(
|
||||
path: &Path,
|
||||
include_trailer: bool,
|
||||
) -> Result<(
|
||||
CpioReader<CompressedReader<BufReader<File>>>,
|
||||
CompressedFormat,
|
||||
)> {
|
||||
let file =
|
||||
File::open(path).with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
|
||||
let reader = CompressedReader::new(BufReader::new(file), true)
|
||||
.with_context(|| format!("Failed to open decompressor: {path:?}"))?;
|
||||
let format = reader.format();
|
||||
let cpio_reader = CpioReader::new(reader, include_trailer);
|
||||
|
||||
Ok((cpio_reader, format))
|
||||
}
|
||||
|
||||
fn open_writer(
|
||||
path: &Path,
|
||||
format: CompressedFormat,
|
||||
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
|
||||
let file =
|
||||
File::create(path).with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
|
||||
let writer = CompressedWriter::new(BufWriter::new(file), format)
|
||||
.with_context(|| format!("Failed to open compressor: {path:?}"))?;
|
||||
let cpio_writer = CpioWriter::new(writer, false);
|
||||
|
||||
Ok(cpio_writer)
|
||||
}
|
||||
|
||||
fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result<()> {
|
||||
let compressed_writer = writer.finish().context("Failed to flush cpio writer")?;
|
||||
let buf_writer = compressed_writer
|
||||
.finish()
|
||||
.context("Failed to flush compressor")?;
|
||||
buf_writer.into_inner().context("Failed to flush file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read cpio information from TOML file.
|
||||
fn read_info(path: &Path) -> Result<CpioInfo> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read cpio info TOML: {path:?}"))?;
|
||||
let info = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse cpio info TOML: {path:?}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Write cpio information to TOML file.
|
||||
fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(info)
|
||||
.with_context(|| format!("Failed to serialize cpio info TOML: {path:?}"))?;
|
||||
fs::write(path, data).with_context(|| format!("Failed to write cpio info TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open reader to the corresponding file inside the tree if the entry is a
|
||||
/// regular file. Unsafe paths will result in an error.
|
||||
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
let path = entry
|
||||
.path
|
||||
.as_bstr()
|
||||
.to_path()
|
||||
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
|
||||
|
||||
let mut reader = tree
|
||||
.open(path)
|
||||
.map(|f| BufReader::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
|
||||
let file_size = reader
|
||||
.seek(std::io::SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {path:?}"))?
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("File is too large: {path:?}"))?;
|
||||
reader
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
Ok(Some((reader, file_size)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open writer to the corresponding file inside the tree if the entry is a
|
||||
/// regular file. Intermediate directories are automatically created as needed.
|
||||
/// Unsafe paths will result in an error.
|
||||
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
let path = entry
|
||||
.path
|
||||
.as_bstr()
|
||||
.to_path()
|
||||
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
|
||||
let parent = util::parent_path(path);
|
||||
|
||||
tree.create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
|
||||
|
||||
let writer = tree
|
||||
.create(path)
|
||||
.map(|f| BufWriter::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
|
||||
Ok(Some(writer))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn display_format(cli: &CpioCli, format: CompressedFormat) {
|
||||
if !cli.quiet {
|
||||
println!("Compression format: {format:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn display_entry(cli: &CpioCli, entry: &CpioEntry) {
|
||||
if !cli.quiet {
|
||||
println!();
|
||||
println!("{entry}");
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_subcommand(
|
||||
cpio_cli: &CpioCli,
|
||||
cli: &UnpackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut reader, format) = open_reader(&cli.input, false)?;
|
||||
let mut info = CpioInfo {
|
||||
format,
|
||||
entries: vec![],
|
||||
};
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_tree, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
|
||||
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
|
||||
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
|
||||
let file_size = entry.data.size()?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
|
||||
.context("Failed to copy data")?;
|
||||
|
||||
writer.into_inner().context("Failed to flush data")?;
|
||||
}
|
||||
|
||||
info.entries.push(entry);
|
||||
}
|
||||
|
||||
write_info(&cli.output_info, &info)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut info = read_info(&cli.input_info)?;
|
||||
let mut writer = open_writer(&cli.output, info.format)?;
|
||||
|
||||
display_format(cpio_cli, info.format);
|
||||
|
||||
if cli.sort {
|
||||
cpio::sort(&mut info.entries);
|
||||
}
|
||||
|
||||
cpio::assign_inodes(&mut info.entries, true)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
|
||||
|
||||
for entry in &mut info.entries {
|
||||
let out = open_tree_file(&tree, entry)?;
|
||||
|
||||
if let Some((_, file_size)) = &out {
|
||||
entry.data = CpioEntryData::Size(*file_size);
|
||||
}
|
||||
|
||||
display_entry(cpio_cli, entry);
|
||||
|
||||
writer
|
||||
.start_entry(entry)
|
||||
.context("Failed to write cpio entry")?;
|
||||
|
||||
if let Some((mut reader, file_size)) = out {
|
||||
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
|
||||
.context("Failed to copy data")?;
|
||||
}
|
||||
}
|
||||
|
||||
flush_writer(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(
|
||||
cpio_cli: &CpioCli,
|
||||
cli: &RepackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut reader, format) = open_reader(&cli.input, false)?;
|
||||
let mut writer = open_writer(&cli.output, format)?;
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
|
||||
writer
|
||||
.start_entry(&entry)
|
||||
.context("Failed to write cpio entry")?;
|
||||
|
||||
if let CpioEntryData::Size(s) = &entry.data {
|
||||
stream::copy_n(&mut reader, &mut writer, u64::from(*s), cancel_signal)
|
||||
.context("Failed to copy cpio entry data")?;
|
||||
}
|
||||
}
|
||||
|
||||
flush_writer(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(cpio_cli: &CpioCli, cli: &InfoCli) -> Result<()> {
|
||||
let (mut reader, format) = open_reader(&cli.input, cli.trailer)?;
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cpio_main(cli: &CpioCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
CpioCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
|
||||
CpioCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
|
||||
CpioCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
|
||||
CpioCommand::Info(c) => info_subcommand(cli, c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack a cpio archive.
|
||||
///
|
||||
/// Regular files will be extracted to the output tree directory, but not any
|
||||
/// other type of file (eg. symlinks). All file metadata is written to the info
|
||||
/// TOML file, like the UID/GID, permissions, and symlink targets.
|
||||
///
|
||||
/// If any paths inside the cpio archive are unsafe, 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 cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
|
||||
output_info: PathBuf,
|
||||
|
||||
/// Path to output files directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
|
||||
output_tree: PathBuf,
|
||||
}
|
||||
|
||||
/// Pack a cpio archive.
|
||||
///
|
||||
/// The new cpio archive will *only* contain files listed in the info TOML file.
|
||||
/// Extra files inside the input tree directory that aren't listed will be
|
||||
/// silently ignored. Entries are added to the archive in the order that they
|
||||
/// are listed unless --sort is specified.
|
||||
///
|
||||
/// All fields inside the info TOML are used as-is. Missing fields in entries
|
||||
/// are set to 0, aside from the inode number, which will be assigned a unique
|
||||
/// value.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to input files directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
|
||||
input_tree: PathBuf,
|
||||
|
||||
/// Sort entries before packing.
|
||||
#[arg(long)]
|
||||
sort: bool,
|
||||
}
|
||||
|
||||
/// Repack a cpio archive.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
/// Display cpio entry information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Show cpio trailer entry.
|
||||
#[arg(long, global = true)]
|
||||
trailer: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CpioCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect cpio archives.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CpioCli {
|
||||
#[command(subcommand)]
|
||||
command: CpioCommand,
|
||||
|
||||
/// Don't print cpio entry information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::fec::FecImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
fn read_fec(path: &Path) -> Result<FecImage> {
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let fec = FecImage::from_reader(reader)
|
||||
.with_context(|| format!("Failed to read FEC data: {path:?}"))?;
|
||||
|
||||
Ok(fec)
|
||||
}
|
||||
|
||||
fn write_fec(path: &Path, fec: &FecImage) -> Result<()> {
|
||||
let mut writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
fec.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write FEC data: {path:?}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush FEC data: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
|
||||
let fec = FecImage::generate(&input, cli.parity, cancel_signal)
|
||||
.context("Failed to generate FEC data")?;
|
||||
|
||||
write_fec(&cli.fec, &fec)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let ranges = cli
|
||||
.range
|
||||
.chunks_exact(2)
|
||||
.map(|w| w[0]..w[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let mut fec = read_fec(&cli.fec)?;
|
||||
|
||||
fec.update(&input, &ranges, cancel_signal)
|
||||
.context("Failed to update FEC data")?;
|
||||
|
||||
write_fec(&cli.fec, &fec)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
|
||||
fec.verify(&input, cancel_signal)
|
||||
.context("Failed to verify data")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, true)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
|
||||
// The separate buffered readers and writers are safe because the function
|
||||
// guarantees that every thread touches disjoint offsets and every offset is
|
||||
// read and written at most once.
|
||||
fec.repair(&input, &input, cancel_signal)
|
||||
.context("Failed to repair file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
FecCommand::Generate(c) => generate_subcommand(c, cancel_signal),
|
||||
FecCommand::Update(c) => update_subcommand(c, cancel_signal),
|
||||
FecCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
FecCommand::Repair(c) => repair_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate FEC data for a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output FEC data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
|
||||
/// Number of parity bytes per RS block (min 2, max 24).
|
||||
#[arg(short, long, value_name = "BYTES", default_value = "2")]
|
||||
parity: u8,
|
||||
}
|
||||
|
||||
/// Update FEC data after a file is modified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UpdateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to FEC data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
|
||||
/// Input file ranges that were updated.
|
||||
///
|
||||
/// This is a half-open range and can be specified multiple times.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
range: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Verify that a file contains no errors.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input FEC data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
}
|
||||
|
||||
/// Repair a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepairCli {
|
||||
/// Path to data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input FEC data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum FecCommand {
|
||||
Generate(GenerateCli),
|
||||
Update(UpdateCli),
|
||||
Verify(VerifyCli),
|
||||
Repair(RepairCli),
|
||||
}
|
||||
|
||||
/// Generate dm-verity FEC data and verify/repair files.
|
||||
///
|
||||
/// These commands operate on FEC files with AOSP's header format.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FecCli {
|
||||
#[command(subcommand)]
|
||||
command: FecCommand,
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::hashtree::HashTreeImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
fn read_hash_tree(path: &Path) -> Result<HashTreeImage> {
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let hash_tree = HashTreeImage::from_reader(reader)
|
||||
.with_context(|| format!("Failed to read hash tree data: {path:?}"))?;
|
||||
|
||||
Ok(hash_tree)
|
||||
}
|
||||
|
||||
fn write_hash_tree(path: &Path, hash_tree: &HashTreeImage) -> Result<()> {
|
||||
let mut writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
hash_tree
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write hash tree data: {path:?}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush hash tree data: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let salt = hex::decode(&cli.salt).context("Invalid salt")?;
|
||||
let input = open_input(&cli.input, false)?;
|
||||
|
||||
let hash_tree =
|
||||
HashTreeImage::generate(&input, cli.block_size, &cli.algorithm, &salt, cancel_signal)
|
||||
.context("Failed to generate hash tree data")?;
|
||||
|
||||
write_hash_tree(&cli.hash_tree, &hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let ranges = cli
|
||||
.range
|
||||
.chunks_exact(2)
|
||||
.map(|w| w[0]..w[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let mut hash_tree = read_hash_tree(&cli.hash_tree)?;
|
||||
|
||||
hash_tree
|
||||
.update(&input, &ranges, cancel_signal)
|
||||
.context("Failed to update hash tree data")?;
|
||||
|
||||
write_hash_tree(&cli.hash_tree, &hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let hash_tree = read_hash_tree(&cli.hash_tree)?;
|
||||
|
||||
hash_tree
|
||||
.verify(&input, cancel_signal)
|
||||
.context("Failed to verify data")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash_tree_main(cli: &HashTreeCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
HashTreeCommand::Generate(c) => generate_subcommand(c, cancel_signal),
|
||||
HashTreeCommand::Update(c) => update_subcommand(c, cancel_signal),
|
||||
HashTreeCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output hash tree data.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
|
||||
/// Block size.
|
||||
#[arg(short, long, value_name = "BYTES", default_value = "4096")]
|
||||
block_size: u32,
|
||||
|
||||
/// Hash algorithm.
|
||||
#[arg(short, long, value_name = "NAME", default_value = "sha256")]
|
||||
algorithm: String,
|
||||
|
||||
/// Salt (in hex).
|
||||
#[arg(short, long, value_name = "HEX", default_value = "")]
|
||||
salt: String,
|
||||
}
|
||||
|
||||
/// Update hash tree data after a file is modified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UpdateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to hash tree data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
|
||||
/// Input file ranges that were updated.
|
||||
///
|
||||
/// This is a half-open range and can be specified multiple times.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
range: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Verify that a file contains no errors.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input hash tree data.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum HashTreeCommand {
|
||||
Generate(GenerateCli),
|
||||
Update(UpdateCli),
|
||||
Verify(VerifyCli),
|
||||
}
|
||||
|
||||
/// Generate dm-verity hash tree data and verify files.
|
||||
///
|
||||
/// These commands operate on a standard hash tree data prepended by a custom
|
||||
/// header.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct HashTreeCli {
|
||||
#[command(subcommand)]
|
||||
command: HashTreeCommand,
|
||||
}
|
||||
+53
-26
@@ -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,
|
||||
@@ -10,7 +8,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
@@ -18,59 +16,71 @@ use crate::{
|
||||
format::avb,
|
||||
};
|
||||
|
||||
fn get_passphrase(group: &PassphraseGroup, key_path: &Path) -> PassphraseSource {
|
||||
if let Some(v) = &group.pass_env_var {
|
||||
PassphraseSource::EnvVar(v.clone())
|
||||
} else if let Some(p) = &group.pass_file {
|
||||
PassphraseSource::File(p.clone())
|
||||
} else {
|
||||
PassphraseSource::Prompt(format!("Enter passphrase for {key_path:?}: "))
|
||||
}
|
||||
fn get_passphrase_source(group: &PassphraseGroup, key_path: &Path) -> PassphraseSource {
|
||||
PassphraseSource::new(
|
||||
key_path,
|
||||
group.pass_file.as_deref(),
|
||||
group.pass_env_var.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
KeyCommand::GenerateKey(c) => {
|
||||
let passphrase = get_passphrase(&c.passphrase, &c.output);
|
||||
let source = get_passphrase_source(&c.passphrase, &c.output);
|
||||
let private_key =
|
||||
crypto::generate_rsa_key_pair().context("Failed to generate RSA keypair")?;
|
||||
|
||||
crypto::write_pem_key_file(&c.output, &private_key, &passphrase)
|
||||
.with_context(|| anyhow!("Failed to write private key: {:?}", c.output))?;
|
||||
crypto::write_pem_key_file(&c.output, &private_key, &source)
|
||||
.with_context(|| format!("Failed to write private key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::GenerateCert(c) => {
|
||||
let passphrase = get_passphrase(&c.passphrase, &c.key);
|
||||
let private_key = crypto::read_pem_key_file(&c.key, &passphrase)
|
||||
.with_context(|| anyhow!("Failed to load key: {:?}", c.key))?;
|
||||
let source = get_passphrase_source(&c.passphrase, &c.key);
|
||||
let private_key = crypto::read_pem_key_file(&c.key, &source)
|
||||
.with_context(|| format!("Failed to load key: {:?}", c.key))?;
|
||||
|
||||
let validity = Duration::from_secs(c.validity * 24 * 60 * 60);
|
||||
let cert = crypto::generate_cert(&private_key, rand::random(), validity, &c.subject)
|
||||
.context("Failed to generate certificate")?;
|
||||
|
||||
crypto::write_pem_cert_file(&c.output, &cert)
|
||||
.with_context(|| anyhow!("Failed to write certificate: {:?}", c.output))?;
|
||||
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::ExtractAvb(c) => {
|
||||
let public_key = if let Some(p) = &c.input.key {
|
||||
let passphrase = get_passphrase(&c.passphrase, p);
|
||||
let passphrase = get_passphrase_source(&c.passphrase, p);
|
||||
let private_key = crypto::read_pem_key_file(p, &passphrase)
|
||||
.with_context(|| anyhow!("Failed to load key: {p:?}"))?;
|
||||
.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(|| anyhow!("Failed to load certificate: {p:?}"))?;
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
|
||||
crypto::get_public_key(&certificate)?
|
||||
crypto::get_public_key(&certificate)
|
||||
.with_context(|| format!("Failed to extract public key: {p:?}"))?
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let encoded = avb::encode_public_key(&public_key)
|
||||
.with_context(|| anyhow!("Failed to encode public key in AVB format"))?;
|
||||
.context("Failed to encode public key in AVB format")?;
|
||||
|
||||
fs::write(&c.output, encoded)
|
||||
.with_context(|| anyhow!("Failed to write public key: {:?}", c.output))?;
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::DecodeAvb(c) => {
|
||||
let encoded = fs::read(&c.key)
|
||||
.with_context(|| format!("Failed to load AVB public key: {:?}", c.key))?;
|
||||
|
||||
let public_key = avb::decode_public_key(&encoded)
|
||||
.context("Failed to decode public key as AVB format")?;
|
||||
|
||||
crypto::write_pem_public_key_file(&c.output, &public_key)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,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>,
|
||||
@@ -153,11 +167,24 @@ struct ExtractAvbCli {
|
||||
passphrase: PassphraseGroup,
|
||||
}
|
||||
|
||||
/// Convert an AVB-encoded public key to a PKCS8-encoded public key.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DecodeAvbCli {
|
||||
/// Path to output PKCS8-encoded public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to AVB-encoded public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum KeyCommand {
|
||||
GenerateKey(GenerateKeyCli),
|
||||
GenerateCert(GenerateCertCli),
|
||||
ExtractAvb(ExtractAvbCli),
|
||||
DecodeAvb(DecodeAvbCli),
|
||||
}
|
||||
|
||||
/// Generate and convert keys.
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
// 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::{bail, Context, Result};
|
||||
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> {
|
||||
match cli_slot {
|
||||
Some(n) => {
|
||||
let n = n as usize;
|
||||
if n >= metadata.slots.len() {
|
||||
bail!("Slot out of range: {n}");
|
||||
}
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
None => {
|
||||
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,
|
||||
}
|
||||
+8
-20
@@ -1,27 +1,15 @@
|
||||
/*
|
||||
* 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;
|
||||
pub mod boot;
|
||||
pub mod completion;
|
||||
pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod key;
|
||||
pub mod lp;
|
||||
pub mod ota;
|
||||
pub mod ramdisk;
|
||||
|
||||
macro_rules! status {
|
||||
($($arg:tt)*) => {
|
||||
println!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! warning {
|
||||
($($arg:tt)*) => {
|
||||
println!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)+))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use status;
|
||||
pub(crate) use warning;
|
||||
pub mod payload;
|
||||
pub mod sparse;
|
||||
|
||||
+1255
-596
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::{anyhow, bail, Context, Result};
|
||||
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,
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
compression::{CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntryNew},
|
||||
},
|
||||
util::EscapedString,
|
||||
};
|
||||
|
||||
static CONTENT_BEGIN: &str = "----- BEGIN UTF-8 CONTENT -----";
|
||||
static CONTENT_END: &str = "----- END UTF-8 CONTENT -----";
|
||||
static CONTENT_END_NO_NEWLINE: &str = "----- END UTF-8 CONTENT (NO NEWLINE) -----";
|
||||
|
||||
static BINARY_BEGIN: &str = "----- BEGIN BINARY CONTENT -----";
|
||||
static BINARY_END: &str = "----- END BINARY CONTENT -----";
|
||||
static BINARY_END_TRUNCATED: &str = "----- END BINARY CONTENT (TRUNCATED) -----";
|
||||
|
||||
static NO_DATA: &str = "----- NO DATA -----";
|
||||
|
||||
fn print_content(data: &[u8], truncate: bool) {
|
||||
if data.is_empty() {
|
||||
println!("{NO_DATA}");
|
||||
return;
|
||||
}
|
||||
|
||||
if !data.contains(&b'\0') {
|
||||
if let Ok(s) = str::from_utf8(data) {
|
||||
if !s.contains(CONTENT_BEGIN)
|
||||
&& !s.contains(CONTENT_END)
|
||||
&& !s.contains(CONTENT_END_NO_NEWLINE)
|
||||
{
|
||||
println!("{CONTENT_BEGIN}");
|
||||
print!("{s}");
|
||||
if data.last() == Some(&b'\n') {
|
||||
println!("{CONTENT_END}");
|
||||
} else {
|
||||
println!();
|
||||
println!("{CONTENT_END_NO_NEWLINE}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{BINARY_BEGIN}");
|
||||
|
||||
if data.len() > 512 && truncate {
|
||||
println!("{}", EscapedString::new_unquoted(&data[..512]));
|
||||
println!("{BINARY_END_TRUNCATED}");
|
||||
} else {
|
||||
println!("{}", EscapedString::new_unquoted(&data));
|
||||
println!("{BINARY_END}");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_archive(
|
||||
path: &Path,
|
||||
include_trailer: bool,
|
||||
) -> Result<(Vec<CpioEntryNew>, CompressedFormat)> {
|
||||
let file = File::open(path)?;
|
||||
let reader = CompressedReader::new(file, true)?;
|
||||
let format = reader.format();
|
||||
let entries = cpio::load(reader, include_trailer)?;
|
||||
|
||||
Ok((entries, format))
|
||||
}
|
||||
|
||||
fn save_archive(path: &Path, entries: &[CpioEntryNew], format: CompressedFormat) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let mut writer = CompressedWriter::new(file, format)?;
|
||||
cpio::save(&mut writer, entries, false)?;
|
||||
writer.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ramdisk_main(cli: &RamdiskCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
RamdiskCommand::Dump(c) => {
|
||||
let (entries, format) = load_archive(&c.input, true)
|
||||
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
|
||||
|
||||
println!("Compression format: {format:?}");
|
||||
println!();
|
||||
|
||||
for entry in entries {
|
||||
println!("{entry}");
|
||||
print_content(&entry.content, !c.no_truncate);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
RamdiskCommand::Repack(c) => {
|
||||
let (entries, format) = load_archive(&c.input, false)
|
||||
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
|
||||
|
||||
save_archive(&c.output, &entries, format)
|
||||
.with_context(|| format!("Failed to write cpio: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump cpio headers and data.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DumpCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Do not truncate binary file contents.
|
||||
#[arg(long)]
|
||||
no_truncate: bool,
|
||||
}
|
||||
|
||||
/// Repack cpio archive.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum RamdiskCommand {
|
||||
Dump(DumpCli),
|
||||
Repack(RepackCli),
|
||||
}
|
||||
|
||||
/// Show information about ramdisk cpio archives.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct RamdiskCli {
|
||||
#[command(subcommand)]
|
||||
command: RamdiskCommand,
|
||||
}
|
||||
@@ -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::{anyhow, bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use crc32fast::Hasher;
|
||||
use zerocopy::{little_endian, IntoBytes};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
padding,
|
||||
sparse::{
|
||||
self, Chunk, ChunkBounds, ChunkData, ChunkList, CrcMode, Header, SparseReader,
|
||||
SparseWriter,
|
||||
},
|
||||
},
|
||||
stream,
|
||||
};
|
||||
|
||||
struct CompactView<'a, T>(&'a [T]);
|
||||
|
||||
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, 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: &mut 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 as i64)) {
|
||||
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 as i64))
|
||||
.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, &mut 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,
|
||||
}
|
||||
+315
-19
@@ -1,14 +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::{
|
||||
env::{self, VarError},
|
||||
ffi::OsString,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -20,14 +19,19 @@ use cms::{
|
||||
SignedData, SignerIdentifier, SignerInfo, SignerInfos,
|
||||
},
|
||||
};
|
||||
use passterm::PromptError;
|
||||
use pkcs8::{
|
||||
pkcs5::{pbes2, scrypt},
|
||||
DecodePrivateKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo, LineEnding,
|
||||
PrivateKeyInfo,
|
||||
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
|
||||
LineEnding, PrivateKeyInfo,
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rsa::{pkcs1v15::SigningKey, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use sha2::Sha256;
|
||||
use rsa::{
|
||||
pkcs1v15::SigningKey, traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{
|
||||
builder::{Builder, CertificateBuilder, Profile},
|
||||
@@ -40,6 +44,24 @@ use x509_cert::{
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Signature algorithm not supported: {0:?}")]
|
||||
UnsupportedAlgorithm(SignatureAlgorithm),
|
||||
#[error("RSA key size ({}) not supported", .0 * 8)]
|
||||
UnsupportedKey(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}")]
|
||||
CommandSpawnFailed(String, #[source] io::Error),
|
||||
#[error("Command failed with status: {1}: {0}")]
|
||||
CommandExecutionFailed(String, 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:?}")]
|
||||
@@ -54,6 +76,10 @@ pub enum Error {
|
||||
SaveKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save unencrypted private key")]
|
||||
SaveKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to RSA sign digest")]
|
||||
RsaSign(#[source] rsa::Error),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("X509 error")]
|
||||
X509(#[from] x509_cert::builder::Error),
|
||||
#[error("SPKI error")]
|
||||
@@ -61,13 +87,41 @@ pub enum Error {
|
||||
#[error("DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
RsaSign(#[from] rsa::Error),
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] 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),
|
||||
@@ -75,13 +129,43 @@ pub enum PassphraseSource {
|
||||
}
|
||||
|
||||
impl PassphraseSource {
|
||||
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
|
||||
if let Some(v) = env_var {
|
||||
Self::EnvVar(v.to_owned())
|
||||
} else if let Some(p) = pass_file {
|
||||
Self::File(p.to_owned())
|
||||
} else {
|
||||
Self::Prompt(format!("Enter passphrase for {key_file:?}: "))
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -100,6 +184,181 @@ impl PassphraseSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_key_size(size: usize) -> Result<()> {
|
||||
// RustCrypto does not support 8192-bit keys.
|
||||
if size > 4096 / 8 {
|
||||
return Err(Error::UnsupportedKey(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 {
|
||||
RsaSigningKey::Internal(key) => key.to_public_key(),
|
||||
RsaSigningKey::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(Error::RsaSign),
|
||||
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::CommandSpawnFailed(format!("{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())?;
|
||||
child.stdin.as_mut().unwrap().write_all(&padded_digest)?;
|
||||
|
||||
let child = child.wait_with_output()?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(Error::CommandExecutionFailed(
|
||||
format!("{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(Error::RsaVerify)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an 4096-bit RSA key pair.
|
||||
pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
|
||||
let mut rng = rand::thread_rng();
|
||||
@@ -154,6 +413,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;
|
||||
|
||||
@@ -163,13 +425,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 {
|
||||
@@ -215,6 +477,41 @@ pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
write_pem_cert(writer, cert)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a reader.
|
||||
pub fn read_pem_public_key(mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
|
||||
let key = RsaPublicKey::from_public_key_pem(&data)?;
|
||||
|
||||
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)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a file.
|
||||
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
read_pem_public_key(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);
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
@@ -321,7 +618,7 @@ pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -359,12 +656,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,
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// 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 thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Decoded string size ({actual}) does not match expected size ({expected})")]
|
||||
BadLength { expected: usize, actual: usize },
|
||||
}
|
||||
|
||||
pub trait FromEscaped: Sized {
|
||||
type Error;
|
||||
|
||||
fn from_escaped(data: &str) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl FromEscaped for Vec<u8> {
|
||||
type Error = Error;
|
||||
|
||||
fn from_escaped(data: &str) -> Result<Self, Self::Error> {
|
||||
Ok(Self::unescape_bytes(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> FromEscaped for [u8; N] {
|
||||
type Error = Error;
|
||||
|
||||
fn from_escaped(data: &str) -> Result<Self, Self::Error> {
|
||||
// Wasteful allocation, but bstr doesn't expose its decoder iterator in
|
||||
// its public API.
|
||||
let decoded = Vec::<u8>::from_escaped(data)?;
|
||||
let mut buf = [0u8; N];
|
||||
|
||||
if decoded.len() != buf.len() {
|
||||
return Err(Error::BadLength {
|
||||
expected: buf.len(),
|
||||
actual: decoded.len(),
|
||||
});
|
||||
}
|
||||
|
||||
buf.copy_from_slice(&decoded);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S, T>(data: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let s = data.as_ref().escape_bytes().to_string();
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
{
|
||||
struct EscapedStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
|
||||
where
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "an escaped string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
FromEscaped::from_escaped(data).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(EscapedStrVisitor(PhantomData))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_vec() {
|
||||
for s in ["", "abc", "你好", "💩", "\x00", "\t\r\n"] {
|
||||
let escaped_s = s.as_bytes().escape_bytes().to_string();
|
||||
let decoded = Vec::<u8>::from_escaped(&escaped_s).unwrap();
|
||||
|
||||
assert_eq!(decoded, s.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_array() {
|
||||
assert_matches!(
|
||||
<[u8; 4]>::from_escaped(r"\t\r\n"),
|
||||
Err(Error::BadLength {
|
||||
expected: 4,
|
||||
actual: 3,
|
||||
})
|
||||
);
|
||||
assert_matches!(
|
||||
<[u8; 4]>::from_escaped(r"\t\r\n\x00\x00"),
|
||||
Err(Error::BadLength {
|
||||
expected: 4,
|
||||
actual: 5,
|
||||
})
|
||||
);
|
||||
assert_matches!(
|
||||
<[u8; 4]>::from_escaped(r"\t\r\n\x00"),
|
||||
Ok(data) if data == *b"\t\r\n\x00"
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
<[u8; 0]>::from_escaped(r"\t"),
|
||||
Err(Error::BadLength {
|
||||
expected: 0,
|
||||
actual: 1,
|
||||
})
|
||||
);
|
||||
assert_matches!(
|
||||
<[u8; 0]>::from_escaped(r""),
|
||||
Ok(data) if data.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serde() {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct TestData {
|
||||
#[serde(with = "super")]
|
||||
contents: Vec<u8>,
|
||||
}
|
||||
|
||||
let mut contents = b"foo\xffbar".to_vec();
|
||||
contents.extend("💩".as_bytes());
|
||||
|
||||
let data = TestData { contents };
|
||||
let serialized = toml_edit::ser::to_string(&data).unwrap();
|
||||
|
||||
assert_eq!(serialized, "contents = 'foo\\xFFbar💩'\n");
|
||||
|
||||
let new_data: TestData = toml_edit::de::from_str(&serialized).unwrap();
|
||||
assert_eq!(data.contents, new_data.contents);
|
||||
}
|
||||
}
|
||||
+864
-635
File diff suppressed because it is too large
Load Diff
+199
-114
@@ -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,11 +10,11 @@ use std::{
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPrivateKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{self, Descriptor, Header},
|
||||
padding,
|
||||
@@ -57,6 +55,15 @@ const VENDOR_HDR_V3_SIZE: u32 = 2112;
|
||||
const VENDOR_HDR_V4_EXTRA_SIZE: u32 = 16;
|
||||
const VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE: u32 = 108;
|
||||
|
||||
/// Maximum size of any individual boot image component, like the kernel. This
|
||||
/// limit is currently 64 MiB, which should be sufficient since there is no
|
||||
/// known device where the entire boot image exceeds this size.
|
||||
const COMPONENT_MAX_SIZE: u32 = 64 * 1024 * 1024;
|
||||
/// Maximum size of the bootconfig component in vendor v4 images. This limit is
|
||||
/// currently 1 KiB, which is ~25x the size of the Pixel 7 Pro stock image's
|
||||
/// bootconfig.
|
||||
const BOOTCONFIG_MAX_SIZE: u32 = 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown boot image format")]
|
||||
@@ -71,8 +78,8 @@ pub enum Error {
|
||||
WriteFieldError(&'static str, #[source] io::Error),
|
||||
#[error("{0:?} field: invalid value: {1}")]
|
||||
InvalidFieldValue(&'static str, u32),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("Invalid data: {0}")]
|
||||
InvalidData(&'static str),
|
||||
#[error("VTS signature is missing hash descriptor")]
|
||||
@@ -257,6 +264,16 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
return Err(Error::UnknownHeaderVersion(header_version));
|
||||
}
|
||||
|
||||
if kernel_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if ramdisk_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
} else if second_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("second_size"));
|
||||
} else if page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
let os_version = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let name = reader
|
||||
@@ -281,6 +298,10 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
|
||||
let mut v1_data = if header_version >= 1 {
|
||||
let recovery_dtbo_size = reader.read_u32::<LittleEndian>()?;
|
||||
if recovery_dtbo_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("recovery_dtbo_size"));
|
||||
}
|
||||
|
||||
let recovery_dtbo_offset = reader.read_u64::<LittleEndian>()?;
|
||||
let header_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
@@ -305,6 +326,10 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
|
||||
let mut v2_data = if header_version == 2 {
|
||||
let dtb_size = reader.read_u32::<LittleEndian>()?;
|
||||
if dtb_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
}
|
||||
|
||||
let dtb_addr = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
let v2_extra = V2Extra {
|
||||
@@ -329,29 +354,39 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
let mut ramdisk = vec![];
|
||||
let mut second = vec![];
|
||||
|
||||
kernel.resize(kernel_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut kernel)?;
|
||||
kernel.resize(kernel_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut kernel)
|
||||
.map_err(|e| Error::ReadFieldError("kernel", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
ramdisk.resize(ramdisk_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut ramdisk)?;
|
||||
ramdisk.resize(ramdisk_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut ramdisk)
|
||||
.map_err(|e| Error::ReadFieldError("ramdisk", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
second.resize(second_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut second)?;
|
||||
second.resize(second_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut second)
|
||||
.map_err(|e| Error::ReadFieldError("second", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
if let Some(v1) = &mut v1_data {
|
||||
v1.v1_extra
|
||||
.recovery_dtbo
|
||||
.resize(v1.recovery_dtbo_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut v1.v1_extra.recovery_dtbo)?;
|
||||
.resize(v1.recovery_dtbo_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut v1.v1_extra.recovery_dtbo)
|
||||
.map_err(|e| Error::ReadFieldError("recovery_dtbo", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
}
|
||||
|
||||
if let Some(v2) = &mut v2_data {
|
||||
v2.v2_extra.dtb.resize(v2.dtb_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut v2.v2_extra.dtb)?;
|
||||
v2.v2_extra.dtb.resize(v2.dtb_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut v2.v2_extra.dtb)
|
||||
.map_err(|e| Error::ReadFieldError("dtb", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
}
|
||||
|
||||
@@ -381,30 +416,35 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, writer: W) -> Result<()> {
|
||||
if self.kernel.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if self.ramdisk.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
} else if self.second.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("second_size"));
|
||||
} else if self.page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
if let Some(v1) = &self.v1_extra {
|
||||
if v1.recovery_dtbo.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("recovery_dtbo_size"));
|
||||
}
|
||||
}
|
||||
if let Some(v2) = &self.v2_extra {
|
||||
if v2.dtb.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let kernel_size = self
|
||||
.kernel
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("kernel_size"))?;
|
||||
let ramdisk_size = self
|
||||
.ramdisk
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("ramdisk_size"))?;
|
||||
let second_size = self
|
||||
.second
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("second_size"))?;
|
||||
|
||||
writer.write_all(&BOOT_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(kernel_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel_addr)?;
|
||||
writer.write_u32::<LittleEndian>(ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk_addr)?;
|
||||
writer.write_u32::<LittleEndian>(second_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.second.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.second_addr)?;
|
||||
writer.write_u32::<LittleEndian>(self.tags_addr)?;
|
||||
writer.write_u32::<LittleEndian>(self.page_size)?;
|
||||
@@ -427,46 +467,44 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
|
||||
.map_err(|e| Error::WriteFieldError("extra_cmdline", e))?;
|
||||
|
||||
if let Some(v1) = &self.v1_extra {
|
||||
let recovery_dtbo_size = v1
|
||||
.recovery_dtbo
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("recovery_dtbo_size"))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(recovery_dtbo_size)?;
|
||||
writer.write_u32::<LittleEndian>(v1.recovery_dtbo.len() as u32)?;
|
||||
writer.write_u64::<LittleEndian>(v1.recovery_dtbo_offset)?;
|
||||
writer.write_u32::<LittleEndian>(self.header_size())?;
|
||||
}
|
||||
|
||||
if let Some(v2) = &self.v2_extra {
|
||||
let dtb_size = v2
|
||||
.dtb
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("dtb_size"))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(dtb_size)?;
|
||||
writer.write_u32::<LittleEndian>(v2.dtb.len() as u32)?;
|
||||
writer.write_u64::<LittleEndian>(v2.dtb_addr)?;
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.kernel)?;
|
||||
writer
|
||||
.write_all(&self.kernel)
|
||||
.map_err(|e| Error::WriteFieldError("kernel", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.ramdisk)?;
|
||||
writer
|
||||
.write_all(&self.ramdisk)
|
||||
.map_err(|e| Error::WriteFieldError("ramdisk", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.second)?;
|
||||
writer
|
||||
.write_all(&self.second)
|
||||
.map_err(|e| Error::WriteFieldError("second", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
if let Some(v1) = &self.v1_extra {
|
||||
writer.write_all(&v1.recovery_dtbo)?;
|
||||
writer
|
||||
.write_all(&v1.recovery_dtbo)
|
||||
.map_err(|e| Error::WriteFieldError("recovery_dtbo", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
}
|
||||
|
||||
if let Some(v2) = &self.v2_extra {
|
||||
writer.write_all(&v2.dtb)?;
|
||||
writer
|
||||
.write_all(&v2.dtb)
|
||||
.map_err(|e| Error::WriteFieldError("dtb", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
}
|
||||
|
||||
@@ -572,12 +610,23 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
|
||||
return Err(Error::UnknownHeaderVersion(header_version));
|
||||
}
|
||||
|
||||
if kernel_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if ramdisk_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
}
|
||||
|
||||
let cmdline = reader
|
||||
.read_string_padded(BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE)
|
||||
.map_err(|e| Error::ReadFieldError("cmdline", e))?;
|
||||
|
||||
let signature_size = if header_version == 4 {
|
||||
Some(reader.read_u32::<LittleEndian>()?)
|
||||
let signature_size = reader.read_u32::<LittleEndian>()?;
|
||||
if signature_size > HDR_V4_SIGNATURE_SIZE as u32 {
|
||||
return Err(Error::FieldOutOfBounds("signature_size"));
|
||||
}
|
||||
|
||||
Some(signature_size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -591,20 +640,26 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
|
||||
let mut kernel = vec![];
|
||||
let mut ramdisk = vec![];
|
||||
|
||||
kernel.resize(kernel_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut kernel)?;
|
||||
kernel.resize(kernel_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut kernel)
|
||||
.map_err(|e| Error::ReadFieldError("kernel", e))?;
|
||||
padding::read_discard(&mut reader, PAGE_SIZE.into())?;
|
||||
|
||||
ramdisk.resize(ramdisk_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut ramdisk)?;
|
||||
ramdisk.resize(ramdisk_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut ramdisk)
|
||||
.map_err(|e| Error::ReadFieldError("ramdisk", e))?;
|
||||
padding::read_discard(&mut reader, PAGE_SIZE.into())?;
|
||||
|
||||
// Don't preserve the signature. It is only used for VTS tests and is
|
||||
// not relevant for booting.
|
||||
let v4_extra = if let Some(s) = signature_size {
|
||||
// OnePlus images have an invalid signature consisting of all zeros.
|
||||
let mut data = vec![0u8; s.to_usize().unwrap()];
|
||||
reader.read_exact(&mut data)?;
|
||||
let mut data = vec![0u8; s as usize];
|
||||
reader
|
||||
.read_exact(&mut data)
|
||||
.map_err(|e| Error::ReadFieldError("signature", e))?;
|
||||
|
||||
let signature = if s > 0 && !util::is_zero(&data) {
|
||||
Some(Header::from_reader(Cursor::new(data))?)
|
||||
@@ -634,22 +689,17 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
|
||||
|
||||
impl BootImageV3Through4 {
|
||||
fn to_writer_internal(&self, writer: impl Write, skip_v4_sig: bool) -> Result<()> {
|
||||
if self.kernel.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if self.ramdisk.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let kernel_size = self
|
||||
.kernel
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("kernel_size"))?;
|
||||
let ramdisk_size = self
|
||||
.ramdisk
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("ramdisk_size"))?;
|
||||
|
||||
writer.write_all(&BOOT_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(kernel_size)?;
|
||||
writer.write_u32::<LittleEndian>(ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.os_version)?;
|
||||
writer.write_u32::<LittleEndian>(self.header_size())?;
|
||||
|
||||
@@ -673,7 +723,7 @@ impl BootImageV3Through4 {
|
||||
|
||||
// The VTS signature is always a fixed size.
|
||||
if size > HDR_V4_SIGNATURE_SIZE {
|
||||
return Err(Error::IntegerTooLarge("signature_size"));
|
||||
return Err(Error::FieldOutOfBounds("signature_size"));
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut sig_writer, HDR_V4_SIGNATURE_SIZE)?;
|
||||
@@ -688,15 +738,21 @@ impl BootImageV3Through4 {
|
||||
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
|
||||
writer.write_all(&self.kernel)?;
|
||||
writer
|
||||
.write_all(&self.kernel)
|
||||
.map_err(|e| Error::WriteFieldError("kernel", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
|
||||
writer.write_all(&self.ramdisk)?;
|
||||
writer
|
||||
.write_all(&self.ramdisk)
|
||||
.map_err(|e| Error::WriteFieldError("ramdisk", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
|
||||
if !skip_v4_sig {
|
||||
if let Some(sig) = v4_signature {
|
||||
writer.write_all(&sig)?;
|
||||
writer
|
||||
.write_all(&sig)
|
||||
.map_err(|e| Error::WriteFieldError("signature", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
}
|
||||
}
|
||||
@@ -707,7 +763,7 @@ impl BootImageV3Through4 {
|
||||
/// Sign the boot image with a legacy VTS signature. Returns true if the
|
||||
/// image was successfully signed. Returns false if there's no vbmeta
|
||||
/// structure to sign in [`V4Extra::signature`].
|
||||
pub fn sign(&mut self, key: &RsaPrivateKey) -> Result<bool> {
|
||||
pub fn sign(&mut self, key: &RsaSigningKey) -> Result<bool> {
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
let image_size;
|
||||
|
||||
@@ -910,9 +966,17 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
}
|
||||
|
||||
let page_size = reader.read_u32::<LittleEndian>()?;
|
||||
if page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
let kernel_addr = reader.read_u32::<LittleEndian>()?;
|
||||
let ramdisk_addr = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let vendor_ramdisk_size = reader.read_u32::<LittleEndian>()?;
|
||||
if vendor_ramdisk_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("vendor_ramdisk_size"));
|
||||
}
|
||||
|
||||
let cmdline = reader
|
||||
.read_string_padded(VENDOR_BOOT_ARGS_SIZE)
|
||||
@@ -925,7 +989,12 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
.map_err(|e| Error::ReadFieldError("name", e))?;
|
||||
|
||||
let header_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let dtb_size = reader.read_u32::<LittleEndian>()?;
|
||||
if dtb_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
}
|
||||
|
||||
let dtb_addr = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
struct V4Data {
|
||||
@@ -938,14 +1007,18 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
let table_size = reader.read_u32::<LittleEndian>()?;
|
||||
let table_entry_num = reader.read_u32::<LittleEndian>()?;
|
||||
let table_entry_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let bootconfig_size = reader.read_u32::<LittleEndian>()?;
|
||||
if bootconfig_size > BOOTCONFIG_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("bootconfig_size"));
|
||||
}
|
||||
|
||||
if table_entry_size != VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE {
|
||||
return Err(Error::InvalidFieldValue(
|
||||
"vendor_ramdisk_table_entry_size",
|
||||
table_entry_size,
|
||||
));
|
||||
} else if table_size != table_entry_num * table_entry_size {
|
||||
} else if table_entry_num.checked_mul(table_entry_size) != Some(table_size) {
|
||||
return Err(Error::InvalidFieldValue(
|
||||
"vendor_ramdisk_table_size",
|
||||
table_size,
|
||||
@@ -972,8 +1045,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
let mut vendor_ramdisk_data = vec![0u8; vendor_ramdisk_size.to_usize().unwrap()];
|
||||
reader.read_exact(&mut vendor_ramdisk_data)?;
|
||||
let mut vendor_ramdisk_data = vec![0u8; vendor_ramdisk_size as usize];
|
||||
reader
|
||||
.read_exact(&mut vendor_ramdisk_data)
|
||||
.map_err(|e| Error::ReadFieldError("vendor_ramdisk_data", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
// For v3, this is just one big ramdisk. For v4, we have to wait until
|
||||
@@ -984,8 +1059,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
vendor_ramdisk_data = vec![];
|
||||
}
|
||||
|
||||
let mut dtb = vec![0u8; dtb_size.to_usize().unwrap()];
|
||||
reader.read_exact(&mut dtb)?;
|
||||
let mut dtb = vec![0u8; dtb_size as usize];
|
||||
reader
|
||||
.read_exact(&mut dtb)
|
||||
.map_err(|e| Error::ReadFieldError("dtb", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
if let Some(v4) = &mut v4_data {
|
||||
@@ -994,6 +1071,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
|
||||
for _ in 0..v4.vendor_ramdisk_table_entry_num {
|
||||
let ramdisk_size = reader.read_u32::<LittleEndian>()?;
|
||||
if ramdisk_size > vendor_ramdisk_size {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
}
|
||||
|
||||
let ramdisk_offset = reader.read_u32::<LittleEndian>()?;
|
||||
let ramdisk_type = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
@@ -1010,7 +1091,7 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
return Err(Error::InvalidFieldValue("ramdisk_offset", ramdisk_offset));
|
||||
}
|
||||
|
||||
let mut ramdisk = vec![0u8; ramdisk_size.to_usize().unwrap()];
|
||||
let mut ramdisk = vec![0u8; ramdisk_size as usize];
|
||||
ramdisk_reader.read_exact(&mut ramdisk)?;
|
||||
ramdisks.push(ramdisk);
|
||||
|
||||
@@ -1033,7 +1114,7 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
v4.v4_extra.bootconfig = reader
|
||||
.read_string_padded(v4.bootconfig_size.to_usize().unwrap())
|
||||
.read_string_padded(v4.bootconfig_size as usize)
|
||||
.map_err(|e| Error::ReadFieldError("bootconfig", e))?;
|
||||
}
|
||||
|
||||
@@ -1070,27 +1151,29 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
return Err(Error::InvalidData("v3 only supports one ramdisk"));
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
let vendor_ramdisk_size = self.ramdisks.iter().map(|r| r.len()).sum::<usize>();
|
||||
if vendor_ramdisk_size > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("vendor_ramdisk_size"));
|
||||
} else if self.dtb.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
} else if self.page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
let vendor_ramdisk_size = self
|
||||
.ramdisks
|
||||
.iter()
|
||||
.map(|r| r.len())
|
||||
.sum::<usize>()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_size"))?;
|
||||
let dtb_size = self
|
||||
.dtb
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("dtb_size"))?;
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
if v4.bootconfig.len() > BOOTCONFIG_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("bootconfig_size"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
writer.write_all(&VENDOR_BOOT_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(self.header_version())?;
|
||||
writer.write_u32::<LittleEndian>(self.page_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel_addr)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk_addr)?;
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_size as u32)?;
|
||||
|
||||
writer
|
||||
.write_string_padded(&self.cmdline, VENDOR_BOOT_ARGS_SIZE)
|
||||
@@ -1103,7 +1186,7 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
.map_err(|e| Error::WriteFieldError("name", e))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(self.header_size())?;
|
||||
writer.write_u32::<LittleEndian>(dtb_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.dtb.len() as u32)?;
|
||||
writer.write_u64::<LittleEndian>(self.dtb_addr)?;
|
||||
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
@@ -1111,38 +1194,37 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
.ramdisks
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_table_entry_num"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("vendor_ramdisk_table_entry_num"))?;
|
||||
let vendor_ramdisk_table_size = vendor_ramdisk_table_entry_num
|
||||
.checked_mul(VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)
|
||||
.and_then(|v| v.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_table_size"))?;
|
||||
let bootconfig_size = v4
|
||||
.bootconfig
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("bootconfig"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("vendor_ramdisk_table_size"))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_table_size)?;
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_table_entry_num)?;
|
||||
writer.write_u32::<LittleEndian>(VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)?;
|
||||
writer.write_u32::<LittleEndian>(bootconfig_size)?;
|
||||
writer.write_u32::<LittleEndian>(v4.bootconfig.len() as u32)?;
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
for ramdisk in &self.ramdisks {
|
||||
writer.write_all(ramdisk)?;
|
||||
writer
|
||||
.write_all(ramdisk)
|
||||
.map_err(|e| Error::WriteFieldError("vendor_ramdisk_data", e))?;
|
||||
}
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.dtb)?;
|
||||
writer
|
||||
.write_all(&self.dtb)
|
||||
.map_err(|e| Error::WriteFieldError("dtb", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
let mut ramdisk_offset = 0;
|
||||
|
||||
for (ramdisk, meta) in self.ramdisks.iter().zip(&v4.ramdisk_metas) {
|
||||
let ramdisk_size = ramdisk.len().to_u32().unwrap();
|
||||
let ramdisk_size = ramdisk.len() as u32;
|
||||
|
||||
writer.write_u32::<LittleEndian>(ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(ramdisk_offset)?;
|
||||
@@ -1160,7 +1242,9 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
}
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(v4.bootconfig.as_bytes())?;
|
||||
writer
|
||||
.write_all(v4.bootconfig.as_bytes())
|
||||
.map_err(|e| Error::WriteFieldError("bootconfig", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
}
|
||||
|
||||
@@ -1169,6 +1253,7 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum BootImage {
|
||||
V0Through2(BootImageV0Through2),
|
||||
V3Through4(BootImageV3Through4),
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
/*
|
||||
* 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::{self, Read, Seek, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] io::Error),
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -96,30 +103,34 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CompressedFormat {
|
||||
None,
|
||||
Gzip,
|
||||
Lz4Legacy,
|
||||
Xz,
|
||||
}
|
||||
|
||||
pub enum CompressedReader<R: Read> {
|
||||
None(R),
|
||||
Gzip(GzDecoder<R>),
|
||||
Lz4(FrameDecoder<R>),
|
||||
Xz(XzDecoder<R>),
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||
let mut magic = [0u8; 4];
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
reader.rewind()?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
} else if &magic == LZ4_LEGACY_MAGIC {
|
||||
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
|
||||
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
||||
} else if &magic == XZ_MAGIC {
|
||||
Ok(Self::Xz(XzDecoder::new(reader)))
|
||||
} else if raw_if_unknown {
|
||||
Ok(Self::None(reader))
|
||||
} else {
|
||||
@@ -132,6 +143,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +152,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
Self::None(r) => r,
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +163,7 @@ impl<R: Read> Read for CompressedReader<R> {
|
||||
Self::None(r) => r.read(buf),
|
||||
Self::Gzip(r) => r.read(buf),
|
||||
Self::Lz4(r) => r.read(buf),
|
||||
Self::Xz(r) => r.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +172,7 @@ pub enum CompressedWriter<W: Write> {
|
||||
None(W),
|
||||
Gzip(GzEncoder<W>),
|
||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||
Xz(XzEncoder<W>),
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedWriter<W> {
|
||||
@@ -168,6 +183,11 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +196,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +205,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::None(w) => Ok(w),
|
||||
Self::Gzip(w) => w.finish(),
|
||||
Self::Lz4Legacy(w) => w.finish(),
|
||||
Self::Xz(w) => w.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +216,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::None(w) => w.write(buf),
|
||||
Self::Gzip(w) => w.write(buf),
|
||||
Self::Lz4Legacy(w) => w.write(buf),
|
||||
Self::Xz(w) => w.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +225,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::None(w) => w.flush(),
|
||||
Self::Gzip(w) => w.flush(),
|
||||
Self::Lz4Legacy(w) => w.flush(),
|
||||
Self::Xz(w) => w.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+559
-142
@@ -1,21 +1,27 @@
|
||||
/*
|
||||
* 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::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
io::{self, Read, Write},
|
||||
io::{self, Cursor, Read, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use num_traits::ToPrimitive;
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
escape,
|
||||
format::padding,
|
||||
stream::{CountingReader, CountingWriter, FromReader, ToWriter, WriteZerosExt},
|
||||
util::{EscapedString, NumBytes},
|
||||
octal,
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt,
|
||||
},
|
||||
util::NumBytes,
|
||||
};
|
||||
|
||||
const MAGIC_NEW: &[u8; 6] = b"070701";
|
||||
@@ -34,16 +40,24 @@ const C_ISCTG: u32 = 0o110000;
|
||||
|
||||
const IO_BLOCK_SIZE: u64 = 512;
|
||||
|
||||
/// The threshold when reading data where memory allocation switches from
|
||||
/// allocating the exact size to resizing as necessary.
|
||||
const VEC_CAP_THRESHOLD: usize = 16384;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
UnknownMagic([u8; 6]),
|
||||
#[error("Hard links are not supported: {0}")]
|
||||
HardLinksNotSupported(EscapedString<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())]
|
||||
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")]
|
||||
IoError(#[from] io::Error),
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -60,7 +74,7 @@ fn read_int(mut reader: impl Read) -> io::Result<u32> {
|
||||
let digit = c.to_digit(16).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{0}: Invalid hex char: {1}", EscapedString::new(&buf), c),
|
||||
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -85,32 +99,200 @@ fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
|
||||
writer.write_all(&buf)
|
||||
}
|
||||
|
||||
fn file_type(mode: u32) -> u32 {
|
||||
mode & 0o170000
|
||||
/// Read a chunk of bytes from the reader. If `size` is less than
|
||||
/// [`VEC_CAP_THRESHOLD`], then the buffer is allocated with the exact size.
|
||||
/// Otherwise, the buffer starts with a capacity of [`VEC_CAP_THRESHOLD`] and
|
||||
/// grows as necessary. This avoids allocating excessive memory when the entry
|
||||
/// specifies an excessively large value that's not backed by actual data.
|
||||
fn read_data(reader: impl Read, size: usize, cancel_signal: &AtomicBool) -> io::Result<Vec<u8>> {
|
||||
let buf = Vec::with_capacity(size.min(VEC_CAP_THRESHOLD));
|
||||
let mut cursor = Cursor::new(buf);
|
||||
|
||||
stream::copy_n(reader, &mut cursor, size as u64, cancel_signal)?;
|
||||
|
||||
Ok(cursor.into_inner())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
pub struct CpioEntryNew {
|
||||
pub ino: u32,
|
||||
pub mode: u32,
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub nlink: u32,
|
||||
pub mtime: u32,
|
||||
pub dev_maj: u32,
|
||||
pub dev_min: u32,
|
||||
pub rdev_maj: u32,
|
||||
pub rdev_min: u32,
|
||||
pub chksum: u32,
|
||||
pub name: Vec<u8>,
|
||||
pub content: Vec<u8>,
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CpioEntryType {
|
||||
Pipe,
|
||||
Char,
|
||||
Directory,
|
||||
Block,
|
||||
Regular,
|
||||
Symlink,
|
||||
Socket,
|
||||
Reserved,
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CpioEntryNew {
|
||||
impl CpioEntryType {
|
||||
pub fn from_mode(mode: u32) -> Self {
|
||||
match mode & 0o170000 {
|
||||
S_IFIFO => Self::Pipe,
|
||||
S_IFCHR => Self::Char,
|
||||
S_IFDIR => Self::Directory,
|
||||
S_IFBLK => Self::Block,
|
||||
S_IFREG => Self::Regular,
|
||||
S_IFLNK => Self::Symlink,
|
||||
S_IFSOCK => Self::Socket,
|
||||
C_ISCTG => Self::Reserved,
|
||||
m => Self::Unknown(m as u16),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mode(self) -> u32 {
|
||||
match self {
|
||||
Self::Pipe => S_IFIFO,
|
||||
Self::Char => S_IFCHR,
|
||||
Self::Directory => S_IFDIR,
|
||||
Self::Block => S_IFBLK,
|
||||
Self::Regular => S_IFREG,
|
||||
Self::Symlink => S_IFLNK,
|
||||
Self::Socket => S_IFSOCK,
|
||||
Self::Reserved => C_ISCTG,
|
||||
Self::Unknown(m) => m.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CpioEntryType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CpioEntryNew")
|
||||
.field("ino", &self.ino)
|
||||
.field("mode", &self.mode)
|
||||
match self {
|
||||
Self::Pipe => write!(f, "pipe"),
|
||||
Self::Char => write!(f, "character device"),
|
||||
Self::Directory => write!(f, "directory"),
|
||||
Self::Block => write!(f, "block device"),
|
||||
Self::Regular => write!(f, "regular file"),
|
||||
Self::Symlink => write!(f, "symbolic link"),
|
||||
Self::Socket => write!(f, "socket"),
|
||||
Self::Reserved => write!(f, "reserved"),
|
||||
Self::Unknown(m) => write!(f, "unknown ({m:o})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CpioEntryType {
|
||||
fn default() -> Self {
|
||||
Self::Unknown(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CpioEntryData {
|
||||
/// Size of entry's data. [`CpioReader`] and [`CpioWriter`] use this for
|
||||
/// [`CpioEntryType::Regular`] entries to allow for lazy reads and writes.
|
||||
Size(u32),
|
||||
/// Entry's data. For [`CpioEntryType::Symlink`] entries, this is the
|
||||
/// link target. For [`CpioEntryType::Regular`] entries, this is the file
|
||||
/// content. [`CpioReader`] will never use this when reading regular files.
|
||||
/// [`CpioWriter`] will write this immediately and lazy writes will not be
|
||||
/// allowed.
|
||||
Data(#[serde(with = "escape")] Vec<u8>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CpioEntryData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Size(s) => f.debug_tuple("Size").field(s).finish(),
|
||||
Self::Data(d) => f.debug_tuple("Data").field(&NumBytes(d.len())).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CpioEntryData {
|
||||
fn default() -> Self {
|
||||
Self::Size(0)
|
||||
}
|
||||
}
|
||||
|
||||
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"))?,
|
||||
};
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn is_size(&self) -> bool {
|
||||
matches!(self, CpioEntryData::Size(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct CpioEntry {
|
||||
/// File path.
|
||||
#[serde(with = "escape")]
|
||||
pub path: Vec<u8>,
|
||||
|
||||
/// File data.
|
||||
#[serde(default, skip_serializing_if = "CpioEntryData::is_size")]
|
||||
pub data: CpioEntryData,
|
||||
|
||||
/// Inode number.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub inode: u32,
|
||||
|
||||
/// File type portion of the `st_mode`-style mode.
|
||||
pub file_type: CpioEntryType,
|
||||
|
||||
/// Permissions portion of the `st_mode`-style mode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero", with = "octal")]
|
||||
pub file_mode: u16,
|
||||
|
||||
/// Owner user ID.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub uid: u32,
|
||||
|
||||
/// Owner group ID.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub gid: u32,
|
||||
|
||||
/// Number of paths referencing the inode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub nlink: u32,
|
||||
|
||||
/// Modification timestamp in Unix time.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub mtime: u32,
|
||||
|
||||
/// Major ID (class of device) for the device containing the inode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub dev_maj: u32,
|
||||
|
||||
/// Minor ID (specific device instance) for the device containing the inode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub dev_min: u32,
|
||||
|
||||
/// Major ID (class of device) represented by this entry. This is only
|
||||
/// relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub rdev_maj: u32,
|
||||
|
||||
/// Minor ID (specific device instance) represented by this entry. This is
|
||||
/// only relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub rdev_min: u32,
|
||||
|
||||
/// CRC32 checksum.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub crc32: u32,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CpioEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CpioEntry")
|
||||
.field("path", &self.path.as_bstr())
|
||||
.field("data", &self.data)
|
||||
.field("inode", &self.inode)
|
||||
.field("file_type", &self.file_type)
|
||||
.field("file_mode", &self.file_mode)
|
||||
.field("uid", &self.uid)
|
||||
.field("gid", &self.gid)
|
||||
.field("nlink", &self.nlink)
|
||||
@@ -119,84 +301,89 @@ impl fmt::Debug for CpioEntryNew {
|
||||
.field("dev_min", &self.dev_min)
|
||||
.field("rdev_maj", &self.rdev_maj)
|
||||
.field("rdev_min", &self.rdev_min)
|
||||
.field("chksum", &self.chksum)
|
||||
.field("name", &EscapedString::new(&self.name))
|
||||
.field("content", &NumBytes(self.content.len()))
|
||||
.field("crc32", &self.crc32)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CpioEntryNew {
|
||||
impl fmt::Display for CpioEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let file_type_str = match file_type(self.mode) {
|
||||
S_IFIFO => Cow::Borrowed("pipe"),
|
||||
S_IFCHR => Cow::Borrowed("character device"),
|
||||
S_IFDIR => Cow::Borrowed("directory"),
|
||||
S_IFBLK => Cow::Borrowed("block device"),
|
||||
S_IFREG => Cow::Borrowed("regular file"),
|
||||
S_IFLNK => Cow::Borrowed("symbolic link"),
|
||||
S_IFSOCK => Cow::Borrowed("socket"),
|
||||
C_ISCTG => Cow::Borrowed("reserved"),
|
||||
m => Cow::Owned(format!("unknown ({m:o})")),
|
||||
};
|
||||
|
||||
writeln!(f, "Filename: {}", EscapedString::new(&self.name))?;
|
||||
writeln!(f, "Filetype: {file_type_str}")?;
|
||||
writeln!(f, "Inode: {}", self.ino)?;
|
||||
writeln!(f, "Mode: {:o}", self.mode)?;
|
||||
writeln!(f, "UID: {}", self.uid)?;
|
||||
writeln!(f, "GID: {}", self.gid)?;
|
||||
writeln!(f, "Links: {}", self.nlink)?;
|
||||
writeln!(f, "Modified: {}", self.mtime)?;
|
||||
writeln!(f, "Device: {:x},{:x}", self.dev_maj, self.dev_min)?;
|
||||
writeln!(f, "Device ID: {:x},{:x}", self.rdev_maj, self.rdev_min)?;
|
||||
writeln!(f, "Checksum: {:x}", self.chksum)?;
|
||||
writeln!(f, "Content: {:?}", NumBytes(self.content.len()))?;
|
||||
writeln!(f, "Path: {:?}", self.path.as_bstr())?;
|
||||
match &self.data {
|
||||
CpioEntryData::Size(s) => {
|
||||
writeln!(f, "Data: {:?}", &NumBytes(*s))?;
|
||||
}
|
||||
CpioEntryData::Data(d) => {
|
||||
if self.file_type == CpioEntryType::Symlink {
|
||||
writeln!(f, "Data: {:?}", d.as_bstr())?;
|
||||
} else {
|
||||
writeln!(f, "Data: {:?}", &NumBytes(d.len()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
writeln!(f, "Inode: {}", self.inode)?;
|
||||
writeln!(f, "Type: {}", self.file_type)?;
|
||||
writeln!(f, "Mode: {:o}", self.file_mode)?;
|
||||
writeln!(f, "UID: {}", self.uid)?;
|
||||
writeln!(f, "GID: {}", self.gid)?;
|
||||
writeln!(f, "Links: {}", self.nlink)?;
|
||||
writeln!(f, "Modtime: {}", self.mtime)?;
|
||||
writeln!(f, "Idevice: {:x},{:x}", self.dev_maj, self.dev_min)?;
|
||||
writeln!(f, "Rdevice: {:x},{:x}", self.rdev_maj, self.rdev_min)?;
|
||||
write!(f, "CRC32: {:x}", self.crc32)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CpioEntryNew {
|
||||
impl CpioEntry {
|
||||
pub fn new_trailer() -> Self {
|
||||
Self {
|
||||
path: CPIO_TRAILER.to_vec(),
|
||||
// Must be 1 for CRC format.
|
||||
nlink: 1,
|
||||
name: CPIO_TRAILER.to_vec(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_symlink(link_target: &[u8], name: &[u8]) -> Self {
|
||||
pub fn new_symlink(path: &[u8], link_target: &[u8]) -> Self {
|
||||
Self {
|
||||
mode: S_IFLNK | 0o777,
|
||||
path: path.to_owned(),
|
||||
data: CpioEntryData::Data(link_target.to_owned()),
|
||||
file_type: CpioEntryType::Symlink,
|
||||
file_mode: 0o777,
|
||||
nlink: 1,
|
||||
name: name.to_owned(),
|
||||
content: link_target.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_directory(name: &[u8]) -> Self {
|
||||
pub fn new_directory(path: &[u8], mode: u16) -> Self {
|
||||
Self {
|
||||
mode: S_IFDIR,
|
||||
path: path.to_owned(),
|
||||
file_type: CpioEntryType::Directory,
|
||||
file_mode: mode,
|
||||
nlink: 1,
|
||||
name: name.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_file(name: &[u8]) -> Self {
|
||||
pub fn new_file(path: &[u8], mode: u16, data: CpioEntryData) -> Self {
|
||||
Self {
|
||||
mode: S_IFREG,
|
||||
path: path.to_owned(),
|
||||
data,
|
||||
file_type: CpioEntryType::Regular,
|
||||
file_mode: mode,
|
||||
nlink: 1,
|
||||
name: name.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_trailer(&self) -> bool {
|
||||
self.path == CPIO_TRAILER
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for CpioEntryNew {
|
||||
impl<R: Read> FromReader<R> for CpioEntry {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
@@ -209,39 +396,61 @@ impl<R: Read> FromReader<R> for CpioEntryNew {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
}
|
||||
|
||||
let ino = read_int(&mut reader)?;
|
||||
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 filesize = 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 namesize = read_int(&mut reader)?;
|
||||
let chksum = read_int(&mut reader)?;
|
||||
let path_size = read_int(&mut reader)?;
|
||||
let crc32 = read_int(&mut reader)?;
|
||||
|
||||
let mut name = vec![0u8; namesize.to_usize().unwrap()];
|
||||
reader.read_exact(&mut name)?;
|
||||
if name.last() != Some(&b'\0') {
|
||||
let mut path = read_data(
|
||||
&mut reader,
|
||||
path_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
if path.last() != Some(&b'\0') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Filename is not NULL-terminated",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
name.pop();
|
||||
path.pop();
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
|
||||
let mut content = vec![0u8; filesize.to_usize().unwrap()];
|
||||
reader.read_exact(&mut content)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
let file_type = CpioEntryType::from_mode(mode);
|
||||
let data = match file_type {
|
||||
// Handled by CpioReader for streaming reads.
|
||||
CpioEntryType::Regular => CpioEntryData::Size(file_size),
|
||||
// Symlinks store the target in the file data.
|
||||
CpioEntryType::Symlink => {
|
||||
let content = read_data(
|
||||
&mut reader,
|
||||
file_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
|
||||
CpioEntryData::Data(content)
|
||||
}
|
||||
// No other entry type should have data.
|
||||
t if file_size != 0 => return Err(Error::EntryHasData(t, path)),
|
||||
_ => CpioEntryData::Size(0),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
ino,
|
||||
mode,
|
||||
path,
|
||||
data,
|
||||
inode,
|
||||
file_type,
|
||||
file_mode: (mode & 0o7777) as u16,
|
||||
uid,
|
||||
gid,
|
||||
nlink,
|
||||
@@ -250,76 +459,236 @@ impl<R: Read> FromReader<R> for CpioEntryNew {
|
||||
dev_min,
|
||||
rdev_maj,
|
||||
rdev_min,
|
||||
chksum,
|
||||
name,
|
||||
content,
|
||||
crc32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for CpioEntryNew {
|
||||
impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, writer: W) -> Result<()> {
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let filesize = self
|
||||
.content
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("filesize"))?;
|
||||
let namesize = self
|
||||
.name
|
||||
let path_size = self
|
||||
.path
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.and_then(|s| s.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("filesize"))?;
|
||||
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
|
||||
|
||||
if self.chksum == 0 {
|
||||
let file_size = self.data.size()?;
|
||||
if file_size != 0
|
||||
&& self.file_type != CpioEntryType::Regular
|
||||
&& self.file_type != CpioEntryType::Symlink
|
||||
{
|
||||
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)?;
|
||||
}
|
||||
|
||||
write_int(&mut writer, self.ino)?;
|
||||
write_int(&mut writer, self.mode)?;
|
||||
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, filesize)?;
|
||||
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, namesize)?;
|
||||
write_int(&mut writer, self.chksum)?;
|
||||
write_int(&mut writer, path_size)?;
|
||||
write_int(&mut writer, self.crc32)?;
|
||||
|
||||
writer.write_all(&self.name)?;
|
||||
writer.write_all(&self.path)?;
|
||||
writer.write_zeros_exact(1)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
|
||||
writer.write_all(&self.content)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
if let CpioEntryData::Data(d) = &self.data {
|
||||
writer.write_all(d)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(mut reader: impl Read, include_trailer: bool) -> Result<Vec<CpioEntryNew>> {
|
||||
let mut entries = vec![];
|
||||
pub struct CpioReader<R: Read> {
|
||||
reader: R,
|
||||
include_trailer: bool,
|
||||
range: Option<Range<u64>>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
loop {
|
||||
let entry = CpioEntryNew::from_reader(&mut reader)?;
|
||||
if file_type(entry.mode) != S_IFDIR && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(EscapedString::new(entry.name)));
|
||||
impl<R: Read> CpioReader<R> {
|
||||
pub fn new(reader: R, include_trailer: bool) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
include_trailer,
|
||||
range: None,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
self.reader
|
||||
}
|
||||
|
||||
fn skip_data(&mut self) -> io::Result<()> {
|
||||
if let Some(range) = &mut self.range {
|
||||
// This cannot overflow because cpio file sizes are 32 bit.
|
||||
let n = range.end - range.start + padding::calc(range.end, 4);
|
||||
self.reader.read_discard_exact(n)?;
|
||||
self.range = None;
|
||||
}
|
||||
|
||||
if entry.name == CPIO_TRAILER {
|
||||
if include_trailer {
|
||||
entries.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_entry(&mut self) -> Result<Option<CpioEntry>> {
|
||||
if self.done {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.skip_data()?;
|
||||
|
||||
let entry = CpioEntry::from_reader(&mut self.reader)?;
|
||||
|
||||
if entry.is_trailer() {
|
||||
self.done = true;
|
||||
|
||||
if !self.include_trailer {
|
||||
return Ok(None);
|
||||
}
|
||||
break;
|
||||
} else if let CpioEntryData::Size(s) = entry.data {
|
||||
self.range = Some(0..u64::from(s));
|
||||
}
|
||||
|
||||
Ok(Some(entry))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for CpioReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let Some(range) = &mut self.range else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"No entry opened",
|
||||
));
|
||||
};
|
||||
|
||||
let to_read = (range.end - range.start).min(buf.len() as u64) as usize;
|
||||
let n = self.reader.read(&mut buf[..to_read])?;
|
||||
|
||||
range.start += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpioWriter<W: Write> {
|
||||
writer: CountingWriter<W>,
|
||||
pad_to_block_size: bool,
|
||||
range: Option<Range<u64>>,
|
||||
max_inode: u32,
|
||||
}
|
||||
|
||||
impl<W: Write> CpioWriter<W> {
|
||||
pub fn new(writer: W, pad_to_block_size: bool) -> Self {
|
||||
Self {
|
||||
writer: CountingWriter::new(writer),
|
||||
pad_to_block_size,
|
||||
range: None,
|
||||
max_inode: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_entry(&mut self) -> io::Result<()> {
|
||||
if let Some(range) = &mut self.range {
|
||||
// This cannot overflow because cpio file sizes are 32 bit.
|
||||
let n = range.end - range.start + padding::calc(range.end, 4);
|
||||
self.writer.write_zeros_exact(n)?;
|
||||
self.range = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
|
||||
self.finish_entry()?;
|
||||
|
||||
entry.to_writer(&mut self.writer)?;
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
self.range = Some(0..u64::from(s));
|
||||
}
|
||||
|
||||
self.max_inode = self.max_inode.max(entry.inode);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.finish_entry()?;
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
Ok(self.writer.finish().0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for CpioWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let Some(range) = &mut self.range else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"No entry started",
|
||||
));
|
||||
};
|
||||
|
||||
let to_write = (range.end - range.start).min(buf.len() as u64) as usize;
|
||||
let n = self.writer.write(&buf[..to_write])?;
|
||||
|
||||
range.start += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
reader: impl Read,
|
||||
include_trailer: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<CpioEntry>> {
|
||||
let mut cpio_reader = CpioReader::new(reader, include_trailer);
|
||||
let mut entries = vec![];
|
||||
|
||||
while let Some(mut entry) = cpio_reader.next_entry()? {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(entry.path.clone()));
|
||||
}
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
|
||||
entry.data = CpioEntryData::Data(data);
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
@@ -328,35 +697,83 @@ pub fn load(mut reader: impl Read, include_trailer: bool) -> Result<Vec<CpioEntr
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn sort(entries: &mut [CpioEntryNew]) {
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
pub fn sort(entries: &mut [CpioEntry]) {
|
||||
entries.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
}
|
||||
|
||||
pub fn reassign_inodes(entries: &mut [CpioEntryNew]) {
|
||||
let mut inode = 300000;
|
||||
|
||||
for entry in entries {
|
||||
entry.ino = inode;
|
||||
inode += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(writer: impl Write, entries: &[CpioEntryNew], pad_to_block_size: bool) -> Result<()> {
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
for entry in entries {
|
||||
entry.to_writer(&mut writer)?;
|
||||
/// Assign inodes to entries. If `missing_only` is true, then inodes are only
|
||||
/// assigned if the inode field in an entry is set to 0.
|
||||
///
|
||||
/// New inodes are assigned starting immediately after the highest inode number
|
||||
/// for a given ([`CpioEntry::dev_maj`], [`CpioEntry::dev_min`]) pair. If there
|
||||
/// are no existing inodes assigned for a device, then the numbers begin at
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
let mut trailer = CpioEntryNew::new_trailer();
|
||||
// 1 higher than the highest inode if possible.
|
||||
trailer.ino = entries.iter().map(|e| e.ino).max().map_or(0, |i| i + 1);
|
||||
trailer.to_writer(&mut writer)?;
|
||||
// (dev maj, dev min) -> (inode set, last assigned inode)
|
||||
let mut inodes: HashMap<(u32, u32), (HashSet<u32>, u32)> = HashMap::new();
|
||||
|
||||
// Pad until the end of the block.
|
||||
if pad_to_block_size {
|
||||
padding::write_zeros(&mut writer, IO_BLOCK_SIZE)?;
|
||||
if missing_only {
|
||||
for entry in &mut *entries {
|
||||
if entry.inode != 0 {
|
||||
let key = (entry.dev_maj, entry.dev_min);
|
||||
let (set, last) = inodes.entry(key).or_default();
|
||||
|
||||
set.insert(entry.inode);
|
||||
*last = (*last).max(entry.inode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
if entry.inode == 0 {
|
||||
let key = (entry.dev_maj, entry.dev_min);
|
||||
let (set, last) = inodes
|
||||
.entry(key)
|
||||
.or_insert_with(|| (HashSet::new(), 299999));
|
||||
|
||||
let mut unused = next_non_zero(*last);
|
||||
|
||||
while set.contains(&unused) {
|
||||
if unused == *last {
|
||||
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
|
||||
}
|
||||
|
||||
unused = next_non_zero(unused);
|
||||
}
|
||||
|
||||
entry.inode = unused;
|
||||
set.insert(unused);
|
||||
*last = unused;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(
|
||||
writer: impl Write,
|
||||
entries: &[CpioEntry],
|
||||
pad_to_block_size: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
for entry in entries {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
cpio_writer.start_entry(entry)?;
|
||||
// CpioEntryData::Data will have already been written.
|
||||
}
|
||||
|
||||
cpio_writer.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,931 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
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 crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
util::{self, NumBytes},
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
const FEC_MAX_BLOCK_SIZE: u32 = 16384;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("FEC with parity byte count of {0} is not supported")]
|
||||
UnsupportedParity(u8),
|
||||
#[error("Cannot calculate FEC for empty data")]
|
||||
InputEmpty,
|
||||
#[error("Input size ({input}) is not a multiple of FEC block size ({block})")]
|
||||
NotBlockAligned { input: u64, block: u32 },
|
||||
#[error("FEC should have size {expected} for input size {input}, but has size {actual}")]
|
||||
InvalidFecSize {
|
||||
input: u64,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("Cannot repair data due to too many errors")]
|
||||
TooManyErrors,
|
||||
#[error("Input data contains errors")]
|
||||
HasErrors,
|
||||
#[error("Data is too small to contain FEC headers")]
|
||||
DataTooSmall,
|
||||
#[error("The two FEC headers are different")]
|
||||
HeadersDifferent,
|
||||
#[error("Invalid FEC header magic: {0:#x}")]
|
||||
InvalidHeaderMagic(u32),
|
||||
#[error("Unsupported FEC header version: {0}")]
|
||||
UnsupportedHeaderVersion(u32),
|
||||
#[error("Invalid FEC header size: {0}")]
|
||||
InvalidHeaderSize(u32),
|
||||
#[error("FEC size in header {value} does not match available data ({available})")]
|
||||
InvalidHeaderFecSize { value: usize, available: usize },
|
||||
#[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),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A small wrapper around a byte array to represent a single Reed-Solomon
|
||||
/// codeword for any `RS(255, K)`.
|
||||
struct Codeword {
|
||||
data: [u8; 255],
|
||||
rs_k: u8,
|
||||
}
|
||||
|
||||
impl Codeword {
|
||||
fn new(rs_k: u8) -> Self {
|
||||
Self {
|
||||
data: [0u8; 255],
|
||||
rs_k,
|
||||
}
|
||||
}
|
||||
|
||||
fn data(&self) -> &[u8] {
|
||||
&self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn data_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn parity(&mut self) -> &[u8] {
|
||||
&self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
fn parity_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
fn all(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn all_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for performing FEC generation, verification, and error correction for
|
||||
/// a specific file size and Reed Solomon configuration. The implementation uses
|
||||
/// dm-verity's interleaving access pattern.
|
||||
///
|
||||
/// The interleaving access pattern can be visualized by placing the file
|
||||
/// offsets in a two-dimensional grid. For example, when reading a 2072576-byte
|
||||
/// file for calculating RS(255, 253):
|
||||
///
|
||||
/// ```text
|
||||
/// | <-------- Round 0 --------> | <-------- Round 1 --------> |
|
||||
/// |-----------------------------|-----------------------------|
|
||||
/// ^ | 0 1 ... 4095 | 4096 4097 ... 8191 |
|
||||
/// | | 8192 8192 ... 12287 | 12288 12289 ... 16383 |
|
||||
/// rs_k | 16384 16385 ... 20479 | 20480 20481 ... 24575 |
|
||||
/// | | ....... ....... ... ....... | ....... ....... ... ....... |
|
||||
/// v | 2064384 2064385 ... 2068479 | 2068480 2068481 ... 2072575 |
|
||||
/// ```
|
||||
///
|
||||
/// A regular sequential read of the file is traversing the grid row-by-row,
|
||||
/// while an interleaving read is traversing the grid column-by-column. Each
|
||||
/// column is always `rs_k` items tall, so each column forms the data portion of
|
||||
/// an RS codeword. The number of columns is always a multiple of the FEC block
|
||||
/// size. Since RS operates on fixed-size codewords and a file size might not
|
||||
/// always fill the grid completely, out-of-bounds offsets are treated as if
|
||||
/// they contain a `\0` byte.
|
||||
///
|
||||
/// All operations are multithreaded with I/O operations parallelized at the
|
||||
/// "round" level and RS operations parallelized at the column level.
|
||||
pub struct Fec {
|
||||
file_size: u64,
|
||||
block_size: u32,
|
||||
rs_k: u8,
|
||||
rounds: u64,
|
||||
}
|
||||
|
||||
impl Fec {
|
||||
pub fn new(file_size: u64, block_size: u32, parity: u8) -> Result<Self> {
|
||||
if file_size == 0 {
|
||||
return Err(Error::InputEmpty);
|
||||
} else if file_size % u64::from(block_size) != 0 {
|
||||
return Err(Error::NotBlockAligned {
|
||||
input: file_size,
|
||||
block: block_size,
|
||||
});
|
||||
} else if block_size > FEC_MAX_BLOCK_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("block_size"));
|
||||
}
|
||||
|
||||
let rs_k = 255 - parity;
|
||||
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
|
||||
return Err(Error::UnsupportedParity(parity));
|
||||
}
|
||||
|
||||
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"))?;
|
||||
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(Self {
|
||||
file_size,
|
||||
block_size,
|
||||
rs_k,
|
||||
rounds,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the number of parity bytes per codeword.
|
||||
#[inline]
|
||||
fn parity(&self) -> u8 {
|
||||
255 - self.rs_k
|
||||
}
|
||||
|
||||
/// Get the size of the FEC data needed to cover the entire file.
|
||||
#[inline]
|
||||
pub fn fec_size(&self) -> usize {
|
||||
usize::from(self.parity()) * self.rounds as usize * self.block_size as usize
|
||||
}
|
||||
|
||||
/// Get the backing file offset for the specified `offset` in the
|
||||
/// interleaved view.
|
||||
fn backing_offset(&self, offset: u64) -> u64 {
|
||||
let rs_k = u64::from(self.rs_k);
|
||||
|
||||
offset / rs_k + offset % rs_k * self.rounds * u64::from(self.block_size)
|
||||
}
|
||||
|
||||
/// Get the rounds that correspond to the specified ranges.
|
||||
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > self.file_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
let mut result = HashSet::new();
|
||||
|
||||
for range in ranges {
|
||||
let start_block = range.start / block_size;
|
||||
let end_block = if range.end % block_size == 0 {
|
||||
range.end / block_size
|
||||
} else {
|
||||
range.end.div_ceil(block_size)
|
||||
};
|
||||
|
||||
for block in start_block..end_block {
|
||||
result.insert(block % self.rounds);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Read a raw sequential block from the backing file, starting at offset
|
||||
/// `offset` in the interleaved view. This reads a horizontal block-aligned
|
||||
/// slice in the file offset grid.
|
||||
fn read_seq_block(
|
||||
&self,
|
||||
mut reader: impl Read + Seek,
|
||||
offset: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<()> {
|
||||
assert_eq!(
|
||||
buf.len(),
|
||||
self.block_size as usize,
|
||||
"Buffer does not match block size",
|
||||
);
|
||||
|
||||
let backing_offset = self.backing_offset(offset);
|
||||
|
||||
// Out of bounds offsets are treated as if they contain zeros.
|
||||
if backing_offset >= self.file_size {
|
||||
buf.fill(0);
|
||||
} else {
|
||||
reader.seek(SeekFrom::Start(backing_offset))?;
|
||||
reader.read_exact(buf)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a raw sequential block to the backing file, starting at offset
|
||||
/// `offset` in the interleaved view. This writes a horizontal block-aligned
|
||||
/// slice in the file offset grid.
|
||||
fn write_seq_block(
|
||||
&self,
|
||||
mut writer: impl Write + Seek,
|
||||
offset: u64,
|
||||
buf: &[u8],
|
||||
) -> io::Result<()> {
|
||||
assert_eq!(
|
||||
buf.len(),
|
||||
self.block_size as usize,
|
||||
"Buffer does not match block size",
|
||||
);
|
||||
|
||||
let backing_offset = self.backing_offset(offset);
|
||||
|
||||
// Out of bounds offsets are ignored.
|
||||
if backing_offset < self.file_size {
|
||||
writer.seek(SeekFrom::Start(backing_offset))?;
|
||||
writer.write_all(buf)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the nth round from the file. The data is laid out sequentially
|
||||
/// (row-by-row).
|
||||
fn read_round(&self, mut reader: impl Read + Seek, round: u64) -> io::Result<Vec<u8>> {
|
||||
let mut grid = vec![0u8; usize::from(self.rs_k) * self.block_size as usize];
|
||||
|
||||
for row in 0..self.rs_k {
|
||||
let interleaved_offset =
|
||||
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
|
||||
let row_start = usize::from(row) * self.block_size as usize;
|
||||
let row_end = row_start + self.block_size as usize;
|
||||
let row_slice = &mut grid[row_start..row_end];
|
||||
|
||||
self.read_seq_block(&mut reader, interleaved_offset, row_slice)?;
|
||||
}
|
||||
|
||||
Ok(grid)
|
||||
}
|
||||
|
||||
/// Write the nth round to the file. The data is expected to be laid out
|
||||
/// sequentially (row-by-row).
|
||||
fn write_round(
|
||||
&self,
|
||||
mut writer: impl Write + Seek,
|
||||
round: u64,
|
||||
grid: &[u8],
|
||||
) -> io::Result<()> {
|
||||
for row in 0..self.rs_k {
|
||||
let interleaved_offset =
|
||||
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
|
||||
let row_start = usize::from(row) * self.block_size as usize;
|
||||
let row_end = row_start + self.block_size as usize;
|
||||
let row_slice = &grid[row_start..row_end];
|
||||
|
||||
self.write_seq_block(&mut writer, interleaved_offset, row_slice)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the nth RS codeword from a round's grid.
|
||||
fn get_codeword(&self, grid: &[u8], column: usize) -> Codeword {
|
||||
let mut codeword = Codeword::new(self.rs_k);
|
||||
let data = codeword.data_mut();
|
||||
|
||||
for row in 0..usize::from(self.rs_k) {
|
||||
data[row] = grid[row * self.block_size as usize + column];
|
||||
}
|
||||
|
||||
codeword
|
||||
}
|
||||
|
||||
/// Put the nth RS codeword into a round's grid.
|
||||
fn put_codeword(&self, grid: &mut [u8], column: usize, codeword: &Codeword) {
|
||||
let data = codeword.data();
|
||||
|
||||
for row in 0..usize::from(self.rs_k) {
|
||||
grid[row * self.block_size as usize + column] = data[row];
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate FEC data for a single round.
|
||||
fn generate_one_round(
|
||||
&self,
|
||||
reader: impl Read + Seek,
|
||||
round: u64,
|
||||
fec: &mut [u8],
|
||||
) -> Result<()> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let encode = verityrs::FN_ENCODE[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
for (column, buf) in fec.chunks_exact_mut(parity).enumerate() {
|
||||
let mut codeword = self.get_codeword(&grid, column);
|
||||
encode(codeword.all_mut());
|
||||
buf.copy_from_slice(codeword.parity());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify file data for a single round.
|
||||
fn verify_one_round(&self, reader: impl Read + Seek, round: u64, fec: &[u8]) -> Result<()> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
for (column, buf) in fec.chunks_exact(parity).enumerate() {
|
||||
let mut codeword = self.get_codeword(&grid, column);
|
||||
codeword.parity_mut().copy_from_slice(buf);
|
||||
|
||||
if !is_correct(codeword.all()) {
|
||||
return Err(Error::HasErrors);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair file data for a single round.
|
||||
fn repair_one_round(
|
||||
&self,
|
||||
reader: impl Read + Seek,
|
||||
writer: impl Write + Seek,
|
||||
round: u64,
|
||||
fec: &[u8],
|
||||
) -> Result<u64> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let mut grid = self.read_round(reader, round)?;
|
||||
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
let mut num_corrected = 0;
|
||||
|
||||
for (column, buf) in fec.chunks_exact(parity).enumerate() {
|
||||
let mut codeword = self.get_codeword(&grid, column);
|
||||
codeword.parity_mut().copy_from_slice(buf);
|
||||
|
||||
let n = correct_errors(codeword.all_mut()).ok_or(Error::TooManyErrors)?;
|
||||
if n > 0 {
|
||||
self.put_codeword(&mut grid, column, &codeword);
|
||||
}
|
||||
|
||||
num_corrected += n as u64;
|
||||
}
|
||||
|
||||
if num_corrected > 0 {
|
||||
self.write_round(writer, round, &grid)?;
|
||||
}
|
||||
|
||||
Ok(num_corrected)
|
||||
}
|
||||
|
||||
/// Generate FEC data for the file. The file size must match the file size
|
||||
/// given to [`Self::new()`].
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn generate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let fec_size = self.fec_size();
|
||||
let mut fec = vec![0u8; fec_size];
|
||||
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(fec)
|
||||
}
|
||||
|
||||
/// Update FEC data coreesponding to the specified file ranges.
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn update(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
fec: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec_size = self.fec_size();
|
||||
if fec.len() != fec_size {
|
||||
return Err(Error::InvalidFecSize {
|
||||
input: self.file_size,
|
||||
expected: fec_size,
|
||||
actual: fec.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let rounds_to_update = self.rounds_for_ranges(ranges)?;
|
||||
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify that the file contains no errors. This is significantly faster
|
||||
/// than [`Self::repair()`] if only error detection, not correction, is
|
||||
/// needed.
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec_size = self.fec_size();
|
||||
if fec.len() != fec_size {
|
||||
return Err(Error::InvalidFecSize {
|
||||
input: self.file_size,
|
||||
expected: fec_size,
|
||||
actual: fec.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fec.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair the file. Up to `parity / 2` bytes per codeword can be repaired.
|
||||
/// If the file is successfully repaired, the number of repaired bytes is
|
||||
/// returned. If the file is corrupt beyond repair, [`Error::TooManyErrors`]
|
||||
/// is returned. It's not safe to assume that as much data as possible has
|
||||
/// been repaired when [`Error::TooManyErrors`] is returned due to fail-fast
|
||||
/// behavior.
|
||||
///
|
||||
/// This function corrects errors at unknown locations only. Correcting
|
||||
/// erasures at known locations is not supported.
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn repair(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
let fec_size = self.fec_size();
|
||||
if fec.len() != fec_size {
|
||||
return Err(Error::InvalidFecSize {
|
||||
input: self.file_size,
|
||||
expected: fec_size,
|
||||
actual: fec.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let num_corrected = fec
|
||||
.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
self.repair_one_round(reader, writer, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
.into_iter()
|
||||
.sum();
|
||||
|
||||
Ok(num_corrected)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for reading and writing AOSP's standalone FEC image format.
|
||||
///
|
||||
/// The FEC data parser in this implementation is strict. All header fields,
|
||||
/// like the version, header size, and digest, must be valid and both copies of
|
||||
/// the header must match.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct FecImage {
|
||||
pub fec: Vec<u8>,
|
||||
pub data_size: u64,
|
||||
pub parity: u8,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FecImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FecImage")
|
||||
.field("fec", &NumBytes(self.fec.len()))
|
||||
.field("data_size", &self.data_size)
|
||||
.field("parity", &self.parity)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FecImage {
|
||||
/// Generate FEC data for a file. `parity` is the number of parity bytes per
|
||||
/// 255-byte Reed-Solomon codeword.
|
||||
pub fn generate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
fec: fec_data,
|
||||
data_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update FEC data coreesponding to the specified file ranges.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.update(input, ranges, &mut self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Check that a file contains no errors. This is significantly faster than
|
||||
/// [`Self::repair()`] if performing a repair is not necessary.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.verify(input, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Repair a file using this instance's FEC data. The maximum correctable
|
||||
/// errors per 255-byte codeword is half of [`Self::parity`]. Returns the
|
||||
/// number of bytes corrected if the file is successfully repaired or
|
||||
/// [`Error::TooManyErrors`] if the file cannot be repaired. This function
|
||||
/// fails fast. If an RS codeword cannot be repaired, other potentially
|
||||
/// repairable codewords may not be repaired.
|
||||
///
|
||||
/// Note that if there are too many errors inside a certain codeword, it's
|
||||
/// possible for there to be a false positive where the corrupted codeword
|
||||
/// is "corrected" into an incorrect value. FEC error detection is not a
|
||||
/// replacement for cryptographically secure digests.
|
||||
///
|
||||
/// The inputs and outputs should point to the same underlying file because
|
||||
/// only regions where errors are corrected are written. It is guaranteed
|
||||
/// that multiple threads will always read and write disjoint file offsets.
|
||||
pub fn repair(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.repair(input, output, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
|
||||
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())?;
|
||||
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for FecImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
// Avoid requiring seekable readers since we need to read everything
|
||||
// into memory anyway.
|
||||
let mut fec = Vec::new();
|
||||
reader.read_to_end(&mut fec)?;
|
||||
|
||||
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];
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
let version = header_reader.read_u32::<LittleEndian>()?;
|
||||
if version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(version));
|
||||
}
|
||||
|
||||
let header_size = header_reader.read_u32::<LittleEndian>()?;
|
||||
if header_size != FEC_HEADER_SIZE as u32 {
|
||||
return Err(Error::InvalidHeaderSize(header_size));
|
||||
}
|
||||
|
||||
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 actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
|
||||
if fec_size != actual_fec_size {
|
||||
return Err(Error::InvalidHeaderFecSize {
|
||||
value: fec_size,
|
||||
available: actual_fec_size,
|
||||
});
|
||||
}
|
||||
|
||||
let input_size = header_reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
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() {
|
||||
return Err(Error::InvalidFecDigest {
|
||||
expected: hex::encode(digest),
|
||||
actual: hex::encode(actual_digest),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
fec,
|
||||
data_size: input_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for FecImage {
|
||||
type Error = Error;
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Seek,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use rand::RngCore;
|
||||
|
||||
use crate::stream::SharedCursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rounds_for_ranges() {
|
||||
let size = 2 * 253 * 4096;
|
||||
let fec = Fec::new(size, 4096, 2).unwrap();
|
||||
|
||||
assert_eq!(fec.rounds_for_ranges(&[0..0]).unwrap(), HashSet::new());
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[0..size]).unwrap(),
|
||||
HashSet::from([0, 1]),
|
||||
);
|
||||
assert_eq!(fec.rounds_for_ranges(&[0..1]).unwrap(), HashSet::from([0]));
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[4095..4096]).unwrap(),
|
||||
HashSet::from([0]),
|
||||
);
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[4095..4097]).unwrap(),
|
||||
HashSet::from([0, 1]),
|
||||
);
|
||||
assert_eq!(
|
||||
fec.rounds_for_ranges(&[size - 1..size]).unwrap(),
|
||||
HashSet::from([1]),
|
||||
);
|
||||
}
|
||||
|
||||
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
|
||||
let mut buf = [0u8; 1];
|
||||
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
|
||||
buf[0] = buf[0].wrapping_add(1);
|
||||
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
file.write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
fn run_test(block_size: u32, rs_k: u8) {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
let parity = 255 - rs_k;
|
||||
|
||||
// Generate data big enough to span multiple rounds, but don't fill the
|
||||
// offset grid to ensure that the out-of-bounds-is-0 behavior works.
|
||||
let size = usize::from(rs_k) * block_size as usize * 3 - block_size as usize;
|
||||
let mut file = SharedCursor::default();
|
||||
let orig_digest = {
|
||||
let mut buf = vec![0u8; size];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
|
||||
let fec = Fec::new(size as u64, block_size, parity).unwrap();
|
||||
assert_eq!(fec.rounds, 3);
|
||||
|
||||
let num_codewords = fec.rounds as usize * block_size as usize;
|
||||
|
||||
// Generate FEC data.
|
||||
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
|
||||
|
||||
// Verify that there are no errors.
|
||||
fec.verify(&file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
// Verify that errors are detected.
|
||||
corrupt_byte(&mut file, 0);
|
||||
assert_matches!(
|
||||
fec.verify(&file, &fec_data, &cancel_signal),
|
||||
Err(Error::HasErrors)
|
||||
);
|
||||
|
||||
// Corrupt one byte in every single codeword.
|
||||
for offset in 1..num_codewords {
|
||||
corrupt_byte(&mut file, offset as u64);
|
||||
}
|
||||
|
||||
// Verify that all the single-byte errors can be fixed. We don't test
|
||||
// for Error::TooManyErrors because of the chance of false positives due
|
||||
// to the nature of RS.
|
||||
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
let repaired_digest = {
|
||||
let mut buf = Vec::new();
|
||||
file.rewind().unwrap();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
|
||||
|
||||
// Intentionally update some data.
|
||||
corrupt_byte(&mut file, 0);
|
||||
let mut fec_data_updated = fec_data.clone();
|
||||
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
|
||||
fec.update(&file, &[0..1], &mut fec_data_updated, &cancel_signal)
|
||||
.unwrap();
|
||||
assert_eq!(fec_data_updated, fec_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_update_verify_repair() {
|
||||
for block_size in [1, 2, 4, 8, 16, 32, 64] {
|
||||
for rs_k in verityrs::FN_ENCODE.keys() {
|
||||
println!("Testing block_size={block_size}, rs_k={rs_k}");
|
||||
run_test(block_size, *rs_k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_image() {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut file = SharedCursor::default();
|
||||
{
|
||||
let mut buf = [0u8; FEC_BLOCK_SIZE];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
let image = FecImage::generate(&file, 2, &cancel_signal).unwrap();
|
||||
|
||||
let mut fec_file = Cursor::new(Vec::new());
|
||||
image.to_writer(&mut fec_file).unwrap();
|
||||
|
||||
fec_file.rewind().unwrap();
|
||||
let new_image = FecImage::from_reader(&mut fec_file).unwrap();
|
||||
|
||||
assert_eq!(image, new_image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
format::{avb, padding},
|
||||
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
|
||||
util::{self, NumBytes},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Hash tree should have size {expected} for input size {input}, but has size {actual}")]
|
||||
InvalidHashTreeSize {
|
||||
input: u64,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("Expected root digest {expected}, but have {actual}")]
|
||||
InvalidRootDigest { expected: String, actual: String },
|
||||
#[error("Expected hash tree {expected}, but have {actual}")]
|
||||
InvalidHashTree { expected: String, actual: String },
|
||||
#[error("Invalid hash tree header magic: {:?}", .0.as_bstr())]
|
||||
InvalidHeaderMagic([u8; 16]),
|
||||
#[error("Invalid hash tree header version: {0}")]
|
||||
InvalidHeaderVersion(u16),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct HashTree {
|
||||
block_size: u32,
|
||||
salted_context: Context,
|
||||
}
|
||||
|
||||
impl HashTree {
|
||||
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &[u8]) -> Self {
|
||||
let mut salted_context = Context::new(algorithm);
|
||||
salted_context.update(salt);
|
||||
|
||||
Self {
|
||||
block_size,
|
||||
salted_context,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of offset ranges that each level occupies in the hash
|
||||
/// tree data. The items are returned with the bottom level's offsets first
|
||||
/// in the list. Note that the bottom level is stored at the end of the hash
|
||||
/// tree data.
|
||||
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 = 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"))?;
|
||||
|
||||
// Depending on the chosen block size, the original file size could
|
||||
// overflow a usize without the first level's size doing the same.
|
||||
let level_size_usize = level_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
|
||||
ranges.push(0..level_size_usize);
|
||||
}
|
||||
|
||||
// The hash tree puts the leaves at the end.
|
||||
let mut offset = 0;
|
||||
for range in ranges.iter_mut().rev() {
|
||||
let level_size = range.end - range.start;
|
||||
range.start += offset;
|
||||
range.end += offset;
|
||||
offset += level_size;
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
/// Convert a list of ranges of byte offsets to a sorted, non-overlapping
|
||||
/// list of block ranges.
|
||||
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > image_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
let mut result = Vec::new();
|
||||
|
||||
for range in ranges {
|
||||
let start_block = range.start / block_size;
|
||||
let end_block = if range.end % block_size == 0 {
|
||||
range.end / block_size
|
||||
} else {
|
||||
range.end.div_ceil(block_size)
|
||||
};
|
||||
|
||||
result.push(start_block..end_block);
|
||||
}
|
||||
|
||||
Ok(util::merge_overlapping(&result))
|
||||
}
|
||||
|
||||
/// Calculate the hash tree digests for a single level of the tree. If the
|
||||
/// reader's position is block-aligned and `image_size` is a multiple of the
|
||||
/// block size, then this function can also be used to calculate the digests
|
||||
/// for a portion of a level.
|
||||
fn hash_partial_level(
|
||||
&self,
|
||||
mut reader: impl Read,
|
||||
mut size: u64,
|
||||
mut level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
// Each digest must be a power of 2.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_padding = algorithm.output_len().next_power_of_two() - algorithm.output_len();
|
||||
let mut buf = vec![0u8; self.block_size as usize];
|
||||
|
||||
while size > 0 {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let n = size.min(buf.len() as u64) as usize;
|
||||
reader.read_exact(&mut buf[..n])?;
|
||||
|
||||
// For undersized blocks, we still hash the whole buffer, except
|
||||
// with padding.
|
||||
buf[n..].fill(0);
|
||||
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
|
||||
// Add the digest to the tree level. Each tree node must be a power
|
||||
// of two.
|
||||
let digest = context.finish();
|
||||
|
||||
level_data[..digest.as_ref().len()].copy_from_slice(digest.as_ref());
|
||||
level_data = &mut level_data[digest.as_ref().len()..];
|
||||
|
||||
level_data[..digest_padding].fill(0);
|
||||
level_data = &mut level_data[digest_padding..];
|
||||
|
||||
size -= n as u64;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hash one full level in parallel.
|
||||
fn hash_one_level_parallel(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
size: u64,
|
||||
level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
assert!(
|
||||
size > self.block_size as u64,
|
||||
"Images smaller than block size must use a normal hash",
|
||||
);
|
||||
|
||||
// Parallelize in larger chunks to avoid too much seek thrashing.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
let multiplier = 1024u64;
|
||||
|
||||
level_data
|
||||
.par_chunks_mut(digest_size * multiplier as usize)
|
||||
.enumerate()
|
||||
.map(|(chunk, out_data)| -> io::Result<()> {
|
||||
let digests = out_data.len() / digest_size;
|
||||
let in_start = (chunk as u64) * multiplier * u64::from(self.block_size);
|
||||
let in_size = ((digests as u64) * u64::from(self.block_size)).min(size - in_start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(in_start))?;
|
||||
|
||||
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update parts of the hash tree level corresponding to the specified
|
||||
/// blocks.
|
||||
fn hash_partial_level_parallel(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
size: u64,
|
||||
block_ranges: &[Range<u64>],
|
||||
level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
|
||||
level_data
|
||||
.par_chunks_exact_mut(digest_size)
|
||||
.enumerate()
|
||||
.filter(|(chunk, _)| util::ranges_contains(block_ranges, &(*chunk as u64)))
|
||||
.map(|(chunk, out_data)| -> io::Result<()> {
|
||||
let in_start = (chunk as u64) * u64::from(self.block_size);
|
||||
let in_size = u64::from(self.block_size).min(size - in_start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(in_start))?;
|
||||
|
||||
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the hash tree and return the root digest. If `ranges` is
|
||||
/// specified, then only the input file blocks containing those ranges are
|
||||
/// recomputed.
|
||||
///
|
||||
/// `hash_tree_data` must match `level_offsets`. In other words, the ending
|
||||
/// offset of the leaf layer of the tree must equal `hash_tree_data`'s size.
|
||||
fn calculate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
level_offsets: &[Range<usize>],
|
||||
hash_tree_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
// Small files are hashed directly.
|
||||
if image_size <= u64::from(self.block_size) {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; image_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
let digest = context.finish();
|
||||
|
||||
return Ok(digest.as_ref().to_vec());
|
||||
}
|
||||
|
||||
// Large files use the hash tree.
|
||||
for (i, level_range) in level_offsets.iter().enumerate() {
|
||||
let (front, back) = hash_tree_data.split_at_mut(level_range.end);
|
||||
let level_data = &mut front[level_range.clone()];
|
||||
|
||||
if i > 0 {
|
||||
// Hash the previous level.
|
||||
let prev_range = level_offsets[i - 1].clone();
|
||||
let prev_size = prev_range.end - prev_range.start;
|
||||
let prev_data = &back[..prev_size];
|
||||
|
||||
self.hash_partial_level(
|
||||
Cursor::new(prev_data),
|
||||
prev_size as u64,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
} else if let Some(r) = ranges {
|
||||
// Read partial blocks from file.
|
||||
let block_ranges = self.blocks_for_ranges(image_size, r)?;
|
||||
|
||||
self.hash_partial_level_parallel(
|
||||
input,
|
||||
image_size,
|
||||
&block_ranges,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
} else {
|
||||
// Read entire file.
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
|
||||
}
|
||||
|
||||
// No need to explicitly ensure the level is padded to the block
|
||||
// size since the tree is initialized with zeros.
|
||||
}
|
||||
|
||||
// Calculate the root hash.
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&hash_tree_data[level_offsets.last().unwrap().clone()]);
|
||||
let root_hash = context.finish().as_ref().to_vec();
|
||||
|
||||
Ok(root_hash)
|
||||
}
|
||||
|
||||
/// Generate hash tree data for the file. Returns the root digest and the
|
||||
/// hash tree data.
|
||||
pub fn generate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let mut hash_tree_data = vec![0u8; hash_tree_size];
|
||||
|
||||
let root_digest = self.calculate(
|
||||
input,
|
||||
image_size,
|
||||
None,
|
||||
&offsets,
|
||||
&mut hash_tree_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok((root_digest, hash_tree_data))
|
||||
}
|
||||
|
||||
/// Update hash tree data corresponding to the specified file ranges.
|
||||
/// Returns the new root digest.
|
||||
pub fn update(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
ranges: &[Range<u64>],
|
||||
hash_tree_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
expected: hash_tree_size,
|
||||
actual: hash_tree_data.len(),
|
||||
});
|
||||
}
|
||||
|
||||
self.calculate(
|
||||
input,
|
||||
image_size,
|
||||
Some(ranges),
|
||||
&offsets,
|
||||
hash_tree_data,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify that the file contains no errors.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
root_digest: &[u8],
|
||||
hash_tree_data: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
expected: hash_tree_size,
|
||||
actual: hash_tree_data.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let (actual_root_digest, actual_hash_tree_data) =
|
||||
self.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
if root_digest != actual_root_digest {
|
||||
return Err(Error::InvalidRootDigest {
|
||||
expected: hex::encode(root_digest),
|
||||
actual: hex::encode(&actual_root_digest),
|
||||
});
|
||||
}
|
||||
|
||||
if hash_tree_data != actual_hash_tree_data {
|
||||
// These are multiple megabytes, so only report the hashes.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let expected = ring::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = ring::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
|
||||
return Err(Error::InvalidHashTree {
|
||||
expected: hex::encode(expected),
|
||||
actual: hex::encode(actual),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for reading and writing a custom hash tree image format.
|
||||
///
|
||||
/// File format:
|
||||
/// - [0 .. 16] - ASCII - "avbroot!hashtree"
|
||||
/// - [16 .. 18] - U16LE - Version
|
||||
/// - [18 .. 26] - U64LE - Image size
|
||||
/// - [26 .. 30] - U32LE - Block size
|
||||
/// - [30 .. 46] - ASCII - Hash algorithm
|
||||
/// - [46 .. 48] - U16LE - Salt size
|
||||
/// - [48 .. 50] - U16LE - Root digest size
|
||||
/// - [50 .. 54] - U32LE - Hash tree size
|
||||
/// - [<variable>] - BINARY - Salt
|
||||
/// - [<variable>] - BINARY - Root digest
|
||||
/// - [<variable>] - BINARY - Hash tree
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct HashTreeImage {
|
||||
pub image_size: u64,
|
||||
pub block_size: u32,
|
||||
pub algorithm: String,
|
||||
pub salt: Vec<u8>,
|
||||
pub root_digest: Vec<u8>,
|
||||
pub hash_tree: Vec<u8>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for HashTreeImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HashTreeImage")
|
||||
.field("block_size", &self.block_size)
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("salt", &hex::encode(&self.salt))
|
||||
.field("root_digest", &hex::encode(&self.root_digest))
|
||||
.field("hash_tree", &NumBytes(self.hash_tree.len()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HashTreeImage {
|
||||
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::ring_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
pub fn generate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
block_size: u32,
|
||||
algorithm: &str,
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let image_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let ring_algorithm = Self::ring_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm: algorithm.to_owned(),
|
||||
salt: salt.to_vec(),
|
||||
root_digest,
|
||||
hash_tree: hash_tree_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update hash tree data coreesponding to the specified file ranges.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
|
||||
self.root_digest = hash_tree.update(
|
||||
input,
|
||||
self.image_size,
|
||||
ranges,
|
||||
&mut self.hash_tree,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that a file contains no errors.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
|
||||
hash_tree.verify(
|
||||
input,
|
||||
self.image_size,
|
||||
&self.root_digest,
|
||||
&self.hash_tree,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
let mut magic = [0u8; 16];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u16::<LittleEndian>()?;
|
||||
if version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(version));
|
||||
}
|
||||
|
||||
let image_size = reader.read_u64::<LittleEndian>()?;
|
||||
let block_size = reader.read_u32::<LittleEndian>()?;
|
||||
let algorithm = reader.read_string_padded(16)?;
|
||||
let salt_size = reader.read_u16::<LittleEndian>()?;
|
||||
let root_digest_size = reader.read_u16::<LittleEndian>()?;
|
||||
let hash_tree_size = reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
|
||||
let mut salt = vec![0u8; usize::from(salt_size)];
|
||||
reader.read_exact(&mut salt)?;
|
||||
|
||||
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
|
||||
reader.read_exact(&mut root_digest)?;
|
||||
|
||||
let mut hash_tree = vec![0u8; hash_tree_size];
|
||||
reader.read_exact(&mut hash_tree)?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
salt,
|
||||
root_digest,
|
||||
hash_tree,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let salt_size = self
|
||||
.salt
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
|
||||
let root_digest_size = self
|
||||
.root_digest
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
|
||||
let hash_tree_size = self
|
||||
.hash_tree
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
|
||||
writer.write_all(Self::MAGIC)?;
|
||||
writer.write_u16::<LittleEndian>(Self::VERSION)?;
|
||||
writer.write_u64::<LittleEndian>(self.image_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.block_size)?;
|
||||
writer.write_string_padded(&self.algorithm, 16)?;
|
||||
writer.write_u16::<LittleEndian>(salt_size)?;
|
||||
writer.write_u16::<LittleEndian>(root_digest_size)?;
|
||||
writer.write_u32::<LittleEndian>(hash_tree_size)?;
|
||||
writer.write_all(&self.salt)?;
|
||||
writer.write_all(&self.root_digest)?;
|
||||
writer.write_all(&self.hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::stream::SharedCursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn calculate_level_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, &[]);
|
||||
assert_eq!(
|
||||
hash_tree.compute_level_offsets(0).unwrap(),
|
||||
&[] as &[Range<usize>],
|
||||
);
|
||||
assert_eq!(
|
||||
hash_tree.compute_level_offsets(1024 * 1024 * 1024).unwrap(),
|
||||
&[69632..8458240, 4096..69632, 0..4096],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_for_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, b"Salt");
|
||||
assert_eq!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16384]).unwrap(),
|
||||
&[0..4],
|
||||
);
|
||||
assert_eq!(hash_tree.blocks_for_ranges(16384, &[0..0]).unwrap(), &[]);
|
||||
assert_eq!(
|
||||
hash_tree
|
||||
.blocks_for_ranges(16384, &[12287..12289, 0..1, 5000..5001])
|
||||
.unwrap(),
|
||||
&[0..4],
|
||||
);
|
||||
assert_matches!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16385]),
|
||||
Err(Error::FieldOutOfBounds(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_update_verify() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
|
||||
let mut input = SharedCursor::new();
|
||||
|
||||
// Try input smaller than one block.
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
root_digest,
|
||||
&[
|
||||
0x15, 0x0f, 0xe5, 0x51, 0x40, 0x30, 0xb1, 0x43, 0x4a, 0x5d, 0xea, 0xf4, 0x91, 0xec,
|
||||
0xe9, 0x2c, 0x0e, 0x64, 0x97, 0x44, 0x7d, 0x6d, 0xe7, 0xbd, 0x6b, 0xa8, 0x5e, 0x8c,
|
||||
0xae, 0x1e, 0x00, 0xa3
|
||||
],
|
||||
);
|
||||
assert_eq!(hash_tree_data, &[]);
|
||||
|
||||
// Try larger input that spans multiple blocks are results in an actual
|
||||
// hash tree being created.
|
||||
input.write_all(&b"Data".repeat(25)).unwrap();
|
||||
|
||||
let (root_digest, mut hash_tree_data) =
|
||||
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
|
||||
assert_eq!(
|
||||
root_digest,
|
||||
&[
|
||||
0x92, 0xc3, 0xd7, 0x4a, 0x64, 0x03, 0x4b, 0xcc, 0xa9, 0x9a, 0x44, 0xf6, 0x81, 0xa2,
|
||||
0x4d, 0xdd, 0x97, 0xd3, 0xda, 0x84, 0xdc, 0xe2, 0x1b, 0x83, 0xd1, 0x7b, 0xab, 0x60,
|
||||
0x59, 0xe8, 0x45, 0x59
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
hash_tree_data,
|
||||
&[
|
||||
0x7e, 0x33, 0x47, 0xb6, 0xf3, 0x7c, 0xde, 0x0e, 0xe2, 0x8d, 0x9e, 0x49, 0x8e, 0xd4,
|
||||
0xbd, 0x53, 0x3a, 0xa1, 0xff, 0xeb, 0x4f, 0x6d, 0x5a, 0x5f, 0x55, 0x28, 0x37, 0x79,
|
||||
0xd0, 0x25, 0x07, 0xd5, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
|
||||
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
|
||||
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
|
||||
],
|
||||
);
|
||||
|
||||
// Change some data and update the hash tree.
|
||||
input.rewind().unwrap();
|
||||
input.write_all(b"Changed").unwrap();
|
||||
|
||||
let root_digest = hash_tree
|
||||
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
root_digest,
|
||||
&[
|
||||
0x8d, 0x03, 0xad, 0x18, 0xf2, 0x53, 0x13, 0x59, 0xf5, 0xbf, 0x68, 0x0e, 0x0c, 0x4a,
|
||||
0x86, 0xe2, 0x6e, 0xaa, 0x3d, 0x4b, 0x0f, 0x1b, 0x57, 0xad, 0x92, 0xe7, 0xbf, 0x3e,
|
||||
0xa6, 0xb1, 0x2e, 0xcc
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
hash_tree_data,
|
||||
&[
|
||||
0xfe, 0x46, 0xf7, 0x8c, 0xa1, 0xd9, 0xc8, 0xdd, 0x47, 0x9e, 0x6c, 0x32, 0x7c, 0x38,
|
||||
0x7f, 0x09, 0xe1, 0x58, 0x92, 0xa3, 0xb6, 0xbd, 0x96, 0xef, 0x10, 0xe8, 0x30, 0xb0,
|
||||
0x37, 0x8d, 0xef, 0x9a, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
|
||||
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
|
||||
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
|
||||
],
|
||||
);
|
||||
|
||||
// Updated hash tree should match newly generated tree.
|
||||
let (new_root_digest, new_hash_tree_data) =
|
||||
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
|
||||
assert_eq!(new_root_digest, root_digest);
|
||||
assert_eq!(new_hash_tree_data, hash_tree_data);
|
||||
|
||||
// Data should validate successfully.
|
||||
hash_tree
|
||||
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
|
||||
.unwrap();
|
||||
|
||||
// But not if the data is corrupted.
|
||||
input.rewind().unwrap();
|
||||
input.write_all(b"Bad").unwrap();
|
||||
|
||||
hash_tree
|
||||
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +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
|
||||
|
||||
pub mod avb;
|
||||
pub mod bootimage;
|
||||
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;
|
||||
|
||||
+403
-96
@@ -1,32 +1,29 @@
|
||||
/*
|
||||
* 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::{
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
iter,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use clap::ValueEnum;
|
||||
use cms::signed_data::SignedData;
|
||||
use const_oid::{db::rfc5912, ObjectIdentifier};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use ring::digest::Context;
|
||||
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha1::Sha1;
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
use x509_cert::{der::Encode, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{mod_OtaMetadata::OtaType, OtaMetadata},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter},
|
||||
util,
|
||||
};
|
||||
|
||||
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
|
||||
@@ -40,10 +37,12 @@ const NAME_PAYLOAD_METADATA: &str = "payload_metadata.bin";
|
||||
pub const PF_NAME: &str = "ota-property-files";
|
||||
pub const PF_STREAMING_NAME: &str = "ota-streaming-property-files";
|
||||
|
||||
const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
|
||||
pub const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
|
||||
|
||||
const COMMENT_MESSAGE: &[u8] = b"signed by avbroot\0";
|
||||
|
||||
const LEGACY_SEP: &str = "|";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Cannot find OTA signature footer magic")]
|
||||
@@ -64,8 +63,12 @@ pub enum Error {
|
||||
UnsupportedDigestAlgorithm(ObjectIdentifier),
|
||||
#[error("Unsupported signature algorithm: {0}")]
|
||||
UnsupportedSignatureAlgorithm(ObjectIdentifier),
|
||||
#[error("Expected entry offsets {0:?}, but have {1:?}")]
|
||||
MismatchedPropertyFiles(String, String),
|
||||
#[error("Invalid legacy metadata line: {0:?}")]
|
||||
InvalidLegacyMetadataLine(String),
|
||||
#[error("Unsupported legacy metadata field: {key:?} = {value:?}")]
|
||||
UnsupportedLegacyMetadataField { key: String, value: String },
|
||||
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
|
||||
MismatchedPropertyFiles { expected: String, actual: String },
|
||||
#[error("Property files {0:?} exceed {1} byte reserved space")]
|
||||
InsufficientReservedSpace(String, usize),
|
||||
#[error("Invalid property file entry: {0:?}")]
|
||||
@@ -76,14 +79,12 @@ pub enum Error {
|
||||
CmsSign(#[from] crypto::Error),
|
||||
#[error("Payload error")]
|
||||
Payload(#[from] payload::Error),
|
||||
#[error("Protobuf error")]
|
||||
Protobuf(#[from] quick_protobuf::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
@@ -92,21 +93,111 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn parse_protobuf_metadata(data: &[u8]) -> Result<OtaMetadata> {
|
||||
Ok(OtaMetadata::decode(data)?)
|
||||
}
|
||||
|
||||
/// Synthesize protobuf structure from legacy plain-text metadata.
|
||||
pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
|
||||
let mut metadata = OtaMetadata::default();
|
||||
|
||||
for line in data.split('\n') {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (key, value) = line
|
||||
.split_once('=')
|
||||
.ok_or_else(|| Error::InvalidLegacyMetadataLine(line.to_owned()))?;
|
||||
let unsupported = || Error::UnsupportedLegacyMetadataField {
|
||||
key: key.to_owned(),
|
||||
value: value.to_owned(),
|
||||
};
|
||||
// Booleans are represented by the presence or absence of `<key>=yes`.
|
||||
let parse_yes = || match value {
|
||||
"yes" => Ok(true),
|
||||
_ => Err(unsupported()),
|
||||
};
|
||||
let parse_list = || {
|
||||
value
|
||||
.split(LEGACY_SEP)
|
||||
.map(|s| s.to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
match key {
|
||||
"ota-type" => {
|
||||
match OtaType::from_str_name(value).ok_or_else(unsupported)? {
|
||||
t @ (OtaType::Ab | OtaType::Block) => metadata.set_type(t),
|
||||
// Not allowed by AOSP in the legacy format.
|
||||
_ => return Err(unsupported()),
|
||||
}
|
||||
}
|
||||
"ota-wipe" => metadata.wipe = parse_yes()?,
|
||||
"ota-retrofit-dynamic-partitions" => {
|
||||
metadata.retrofit_dynamic_partitions = parse_yes()?
|
||||
}
|
||||
"ota-downgrade" => metadata.downgrade = parse_yes()?,
|
||||
"ota-required-cache" => {
|
||||
metadata.required_cache = value.parse().map_err(|_| unsupported())?;
|
||||
}
|
||||
"post-build" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.build = parse_list();
|
||||
}
|
||||
"post-build-incremental" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
value.clone_into(&mut p.build_incremental);
|
||||
}
|
||||
"post-sdk-level" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
value.clone_into(&mut p.sdk_level);
|
||||
}
|
||||
"post-security-patch-level" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
value.clone_into(&mut p.security_patch_level);
|
||||
}
|
||||
"post-timestamp" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.timestamp = value.parse().map_err(|_| unsupported())?;
|
||||
}
|
||||
"pre-device" => {
|
||||
let p = metadata.precondition.get_or_insert_with(Default::default);
|
||||
p.device = parse_list();
|
||||
}
|
||||
"pre-build" => {
|
||||
let p = metadata.precondition.get_or_insert_with(Default::default);
|
||||
p.build = parse_list();
|
||||
}
|
||||
"pre-build-incremental" => {
|
||||
let p = metadata.precondition.get_or_insert_with(Default::default);
|
||||
value.clone_into(&mut p.build_incremental);
|
||||
}
|
||||
"spl-downgrade" => metadata.spl_downgrade = parse_yes()?,
|
||||
k if k.ends_with("-property-files") => {
|
||||
metadata
|
||||
.property_files
|
||||
.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
_ => {
|
||||
// Ignore. Some OEMs insert values that aren't defined in AOSP.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Generate the legacy plain-text and modern protobuf serializations of the
|
||||
/// given metadata instance.
|
||||
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
const SEP: &str = "|";
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut pairs = BTreeMap::<String, String>::new();
|
||||
|
||||
match metadata.type_pb {
|
||||
OtaType::AB => {
|
||||
pairs.insert("ota-type".to_owned(), "AB".to_owned());
|
||||
}
|
||||
OtaType::BLOCK => {
|
||||
pairs.insert("ota-type".to_owned(), "BLOCK".to_owned());
|
||||
}
|
||||
_ => {}
|
||||
// Other types are not allowed by AOSP in the legacy format.
|
||||
if let t @ (OtaType::Ab | OtaType::Block) = metadata.r#type() {
|
||||
pairs.insert("ota-type".to_owned(), t.as_str_name().to_owned());
|
||||
}
|
||||
if metadata.wipe {
|
||||
pairs.insert("ota-wipe".to_owned(), "yes".to_owned());
|
||||
@@ -127,7 +218,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
);
|
||||
|
||||
if let Some(p) = &metadata.postcondition {
|
||||
pairs.insert("post-build".to_owned(), p.build.join(SEP));
|
||||
pairs.insert("post-build".to_owned(), p.build.join(LEGACY_SEP));
|
||||
pairs.insert(
|
||||
"post-build-incremental".to_owned(),
|
||||
p.build_incremental.clone(),
|
||||
@@ -141,9 +232,9 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
}
|
||||
|
||||
if let Some(p) = &metadata.precondition {
|
||||
pairs.insert("pre-device".to_owned(), p.device.join(SEP));
|
||||
pairs.insert("pre-device".to_owned(), p.device.join(LEGACY_SEP));
|
||||
if !p.build.is_empty() {
|
||||
pairs.insert("pre-build".to_owned(), p.build.join(SEP));
|
||||
pairs.insert("pre-build".to_owned(), p.build.join(LEGACY_SEP));
|
||||
pairs.insert(
|
||||
"pre-build-incremental".to_owned(),
|
||||
p.build_incremental.clone(),
|
||||
@@ -157,11 +248,11 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
|
||||
pairs.extend(metadata.property_files.clone());
|
||||
|
||||
let legacy_metadata = pairs
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{k}={v}\n"))
|
||||
.collect::<String>();
|
||||
let modern_metadata = util::write_protobuf(metadata)?;
|
||||
let legacy_metadata = pairs.into_iter().fold(String::new(), |mut output, (k, v)| {
|
||||
let _ = writeln!(output, "{k}={v}");
|
||||
output
|
||||
});
|
||||
let modern_metadata = metadata.encode_to_vec();
|
||||
|
||||
Ok((legacy_metadata, modern_metadata))
|
||||
}
|
||||
@@ -283,12 +374,24 @@ fn add_payload_metadata_entry(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum ZipMode {
|
||||
Streaming,
|
||||
Seekable,
|
||||
}
|
||||
|
||||
impl fmt::Display for ZipMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add metadata files to the output OTA zip. `zip_entries` is the list of
|
||||
/// [`ZipEntry`] already written to `zip_writer`. `next_offset` is the current
|
||||
/// file offset (where the next zip entry's local header begins).
|
||||
/// `metadata_pb_raw` is the serialized OTA metadata protobuf message from the
|
||||
/// original OTA. `payload_metadata_size` is the size of the new payload's
|
||||
/// metadata and metadata signature regions.
|
||||
/// `metadata` is the OTA metadata protobuf message from the original OTA.
|
||||
/// `payload_metadata_size` is the size of the new payload's metadata and
|
||||
/// metadata signature regions.
|
||||
///
|
||||
/// The zip file's backing file position MUST BE set to where the central
|
||||
/// directory would start.
|
||||
@@ -296,10 +399,11 @@ pub fn add_metadata(
|
||||
zip_entries: &[ZipEntry],
|
||||
zip_writer: &mut ZipWriter<impl Write>,
|
||||
next_offset: u64,
|
||||
metadata_pb_raw: &[u8],
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
zip_mode: ZipMode,
|
||||
) -> Result<OtaMetadata> {
|
||||
let mut metadata: OtaMetadata = util::read_protobuf(metadata_pb_raw)?;
|
||||
let mut metadata = metadata.clone();
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
|
||||
let mut zip_entries = zip_entries.to_owned();
|
||||
@@ -318,7 +422,11 @@ pub fn add_metadata(
|
||||
// Add the placeholders to a temporary zip to compute final property files.
|
||||
let (temp_legacy_offset, temp_modern_offset) = {
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
|
||||
let mut writer = ZipWriter::new_streaming(Cursor::new(Vec::new()));
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = match zip_mode {
|
||||
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => ZipWriter::new(raw_writer),
|
||||
};
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = writer.end_extra_data()?;
|
||||
@@ -389,7 +497,10 @@ pub fn verify_metadata(
|
||||
for (key, value) in &metadata.property_files {
|
||||
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
if *value != new_value {
|
||||
return Err(Error::MismatchedPropertyFiles(value.clone(), new_value));
|
||||
return Err(Error::MismatchedPropertyFiles {
|
||||
expected: value.clone(),
|
||||
actual: new_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,10 +563,7 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
/// CMS signed attributes are intentionally not supported because AOSP recovery
|
||||
/// does not support them either. It expects the CMS [`SignedData`] structure to
|
||||
/// be used for nothing more than a raw signature transport mechanism.
|
||||
pub fn verify_ota(
|
||||
mut reader: impl Read + Seek,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
) -> Result<Certificate> {
|
||||
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
|
||||
let (sd, hashed_size) = parse_ota_sig(&mut reader)?;
|
||||
|
||||
// Make sure the certificate in the CMS structure matches the otacert zip
|
||||
@@ -490,12 +598,12 @@ pub fn verify_ota(
|
||||
reader.seek(SeekFrom::Start(0))?;
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (algorithm, scheme) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, Pkcs1v15Sign::new::<Sha256>())
|
||||
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
|
||||
} else {
|
||||
(
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
Pkcs1v15Sign::new::<Sha1>(),
|
||||
SignatureAlgorithm::Sha1WithRsa,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -507,7 +615,7 @@ pub fn verify_ota(
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key.verify(scheme, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
public_key.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
|
||||
Ok(cert.clone())
|
||||
}
|
||||
@@ -521,9 +629,9 @@ pub fn parse_zip_ota_info(
|
||||
|
||||
let metadata = {
|
||||
let mut entry = zip.by_name(PATH_METADATA_PB)?;
|
||||
let mut buf = vec![0u8; entry.size() as usize];
|
||||
entry.read_exact(&mut buf)?;
|
||||
util::read_protobuf::<OtaMetadata>(&buf)?
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf)?;
|
||||
OtaMetadata::decode(buf.as_slice())?
|
||||
};
|
||||
|
||||
let certificate = {
|
||||
@@ -546,17 +654,83 @@ pub fn parse_zip_ota_info(
|
||||
Ok((metadata, certificate, header, properties))
|
||||
}
|
||||
|
||||
/// Ensure that we're using a non-zip64 EOCD and there's no archive comment.
|
||||
fn validate_eocd(eocd: &[u8]) -> io::Result<()> {
|
||||
if &eocd[..4] != b"PK\x05\x06" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"EOCD magic not found",
|
||||
));
|
||||
} else if &eocd[20..22] != b"\0\0" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Archive comment is not 0 bytes",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the digital signature for the specified digest, formatted as a zip
|
||||
/// file archive comment. The returned buffer includes both the 2-byte comment
|
||||
/// size field and the comment itself. It should be written to the end of the
|
||||
/// zip file after truncating the original 2-byte comment size field.
|
||||
fn compute_signature_comment(
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
digest: ring::digest::Digest,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cms_signature = crypto::cms_sign_external(key, cert, digest.as_ref())?;
|
||||
let cms_signature_der = cms_signature.to_der()?;
|
||||
|
||||
// Includes placeholder for the EOCD comment size field.
|
||||
let mut buf = vec![0; 2];
|
||||
|
||||
// NULL-terminated readable message and actual signature.
|
||||
buf.extend(COMMENT_MESSAGE);
|
||||
buf.extend(&cms_signature_der);
|
||||
|
||||
// 6-byte OTA footer.
|
||||
let comment_size = buf.len() - 2 + 6;
|
||||
|
||||
// Absolute value of the offset of the signature from the end of the archive
|
||||
// comment.
|
||||
buf.extend((cms_signature_der.len() as u16 + 6).to_le_bytes());
|
||||
|
||||
// Magic value.
|
||||
buf.extend(b"\xff\xff");
|
||||
|
||||
// Archive comment size (for use by the OTA signature verifier).
|
||||
buf.extend(((comment_size) as u16).to_le_bytes());
|
||||
|
||||
if let Some(o) = memmem::find(&buf[2..], ZIP_EOCD_MAGIC) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Archive comment contains EOCD magic at offset {o}"),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Archive comment size (for the EOCD comment size field).
|
||||
buf[..2].copy_from_slice(&((comment_size) as u16).to_le_bytes());
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// A writer that produces a signapk-style signed zip file with a whole-file
|
||||
/// signature stored in the zip archive comment. The data will be left in an
|
||||
/// unusable state if [`Self::finish()`] is not called.
|
||||
pub struct SigningWriter<W: Write> {
|
||||
///
|
||||
/// This writer works with streaming zip files created with data descriptors.
|
||||
/// The data is hashed as it is being written.
|
||||
pub struct StreamingSigningWriter<W> {
|
||||
inner: HashingWriter<W>,
|
||||
// Android only supports non-zip64 EOCD.
|
||||
queue: [u8; 22],
|
||||
used: usize,
|
||||
}
|
||||
|
||||
impl<W: Write> SigningWriter<W> {
|
||||
impl<W: Write> StreamingSigningWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner: HashingWriter::new(inner, Context::new(&ring::digest::SHA256)),
|
||||
@@ -565,64 +739,29 @@ impl<W: Write> SigningWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self, key: &RsaPrivateKey, cert: &Certificate) -> Result<W> {
|
||||
pub fn finish(mut self, key: &RsaSigningKey, cert: &Certificate) -> Result<W> {
|
||||
if self.used < self.queue.len() {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
|
||||
);
|
||||
} else if &self.queue[..4] != b"PK\x05\x06" {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "EOCD magic not found").into());
|
||||
} else if &self.queue[20..22] != b"\0\0" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Archive comment is not 0 bytes",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
validate_eocd(&self.queue)?;
|
||||
|
||||
// Chop off the archive comment size field and write the remaining data.
|
||||
self.inner.write_all(&self.queue[..20])?;
|
||||
|
||||
let (mut raw_writer, context) = self.inner.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
let cms_signature = crypto::cms_sign_external(key, cert, digest.as_ref())?;
|
||||
let cms_signature_der = cms_signature.to_der()?;
|
||||
|
||||
let mut comment = COMMENT_MESSAGE.to_vec();
|
||||
comment.extend(&cms_signature_der);
|
||||
|
||||
let comment_size = comment.len() + 6;
|
||||
|
||||
// Absolute value of the offset of the signature from the end of the
|
||||
// archive comment.
|
||||
comment.extend((cms_signature_der.len() as u16 + 6).to_le_bytes());
|
||||
|
||||
// Magic value.
|
||||
comment.extend(b"\xff\xff");
|
||||
|
||||
// EOCD archive comment size.
|
||||
comment.extend(((comment_size) as u16).to_le_bytes());
|
||||
|
||||
if let Some(o) = memmem::find(&comment, ZIP_EOCD_MAGIC) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Archive comment contains EOCD magic at offset {o}"),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Write EOCD comment size field, which was removed before.
|
||||
raw_writer.write_all(&((comment_size) as u16).to_le_bytes())?;
|
||||
|
||||
// Finally, write the comment.
|
||||
raw_writer.write_all(&comment)?;
|
||||
let size_and_comment = compute_signature_comment(key, cert, digest)?;
|
||||
raw_writer.write_all(&size_and_comment)?;
|
||||
|
||||
Ok(raw_writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SigningWriter<W> {
|
||||
impl<W: Write> Write for StreamingSigningWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let (front, back) = buf.split_at(buf.len().saturating_sub(self.queue.len()));
|
||||
if front.is_empty() {
|
||||
@@ -654,3 +793,171 @@ impl<W: Write> Write for SigningWriter<W> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer that produces a signapk-style signed zip file with a whole-file
|
||||
/// signature stored in the zip archive comment. The data will be left in an
|
||||
/// unusable state if [`Self::finish()`] is not called.
|
||||
///
|
||||
/// This writer works with zip files written without data descriptors. The data
|
||||
/// is hashed during [`Self::finish()`].
|
||||
pub struct SeekableSigningWriter<W> {
|
||||
inner: W,
|
||||
}
|
||||
|
||||
impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn finish(
|
||||
mut self,
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<W> {
|
||||
let file_size = self.seek(SeekFrom::End(0))?;
|
||||
|
||||
// Android only supports non-zip64 EOCD.
|
||||
if file_size < 22 {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
|
||||
);
|
||||
}
|
||||
|
||||
self.seek_relative(-22)?;
|
||||
let mut eocd = [0u8; 22];
|
||||
self.read_exact(&mut eocd)?;
|
||||
|
||||
validate_eocd(&eocd)?;
|
||||
|
||||
// Compute the digest of everything up until the comment size field.
|
||||
let mut hashing_writer = HashingWriter::new(
|
||||
io::sink(),
|
||||
ring::digest::Context::new(&ring::digest::SHA256),
|
||||
);
|
||||
|
||||
self.rewind()?;
|
||||
stream::copy_n(&mut self, &mut hashing_writer, file_size - 2, cancel_signal)?;
|
||||
|
||||
let digest = hashing_writer.finish().1.finish();
|
||||
|
||||
let size_and_comment = compute_signature_comment(key, cert, digest)?;
|
||||
self.inner.write_all(&size_and_comment)?;
|
||||
|
||||
Ok(self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Read> Read for SeekableSigningWriter<W> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SeekableSigningWriter<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: Seek> Seek for SeekableSigningWriter<W> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum SigningWriterInner<W> {
|
||||
Streaming {
|
||||
inner: StreamingSigningWriter<W>,
|
||||
},
|
||||
Seekable {
|
||||
inner: SeekableSigningWriter<W>,
|
||||
/// We need to store this because [`SigningWriter::finish`] needs to be
|
||||
/// in `impl<W: Write>`, where [`SeekableSigningWriter::finish`] is not
|
||||
/// available.
|
||||
finish: fn(
|
||||
SeekableSigningWriter<W>,
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<W>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A writer that produces a signapk-style signed zip file with a whole-file
|
||||
/// signature stored in the zip archive comment. The data will be left in an
|
||||
/// unusable state if [`Self::finish()`] is not called.
|
||||
///
|
||||
/// This is a partially type-erased wrapper around [`StreamingSigningWriter`]
|
||||
/// and [`SeekableSigningWriter`].
|
||||
pub struct SigningWriter<W>(SigningWriterInner<W>);
|
||||
|
||||
impl<W: Write> SigningWriter<W> {
|
||||
pub fn new_streaming(inner: W) -> Self {
|
||||
Self(SigningWriterInner::Streaming {
|
||||
inner: StreamingSigningWriter::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finish(
|
||||
self,
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<W> {
|
||||
match self.0 {
|
||||
SigningWriterInner::Streaming { inner } => inner.finish(key, cert),
|
||||
SigningWriterInner::Seekable { inner, finish } => {
|
||||
finish(inner, key, cert, cancel_signal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Read + Write + Seek> SigningWriter<W> {
|
||||
pub fn new_seekable(inner: W) -> Self {
|
||||
Self(SigningWriterInner::Seekable {
|
||||
inner: SeekableSigningWriter::new(inner),
|
||||
finish: SeekableSigningWriter::finish,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Read> Read for SigningWriter<W> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match &mut self.0 {
|
||||
SigningWriterInner::Streaming { .. } => panic!("Called with streaming writer"),
|
||||
SigningWriterInner::Seekable { inner, .. } => inner.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SigningWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match &mut self.0 {
|
||||
SigningWriterInner::Streaming { inner } => inner.write(buf),
|
||||
SigningWriterInner::Seekable { inner, .. } => inner.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match &mut self.0 {
|
||||
SigningWriterInner::Streaming { inner } => inner.flush(),
|
||||
SigningWriterInner::Seekable { inner, .. } => inner.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Seek> Seek for SigningWriter<W> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
match &mut self.0 {
|
||||
SigningWriterInner::Streaming { .. } => panic!("Called with streaming writer"),
|
||||
SigningWriterInner::Seekable { inner, .. } => inner.seek(pos),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::{self, Read, Seek, Write};
|
||||
|
||||
|
||||
+507
-212
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
// 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
|
||||
// implementations for every supported configuration.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use gf256::rs::rs;
|
||||
use phf::phf_map;
|
||||
|
||||
#[rs(block = 255, data = 231)]
|
||||
mod rs255w231 {}
|
||||
#[rs(block = 255, data = 232)]
|
||||
mod rs255w232 {}
|
||||
#[rs(block = 255, data = 233)]
|
||||
mod rs255w233 {}
|
||||
#[rs(block = 255, data = 234)]
|
||||
mod rs255w234 {}
|
||||
#[rs(block = 255, data = 235)]
|
||||
mod rs255w235 {}
|
||||
#[rs(block = 255, data = 236)]
|
||||
mod rs255w236 {}
|
||||
#[rs(block = 255, data = 237)]
|
||||
mod rs255w237 {}
|
||||
#[rs(block = 255, data = 238)]
|
||||
mod rs255w238 {}
|
||||
#[rs(block = 255, data = 239)]
|
||||
mod rs255w239 {}
|
||||
#[rs(block = 255, data = 240)]
|
||||
mod rs255w240 {}
|
||||
#[rs(block = 255, data = 241)]
|
||||
mod rs255w241 {}
|
||||
#[rs(block = 255, data = 242)]
|
||||
mod rs255w242 {}
|
||||
#[rs(block = 255, data = 243)]
|
||||
mod rs255w243 {}
|
||||
#[rs(block = 255, data = 244)]
|
||||
mod rs255w244 {}
|
||||
#[rs(block = 255, data = 245)]
|
||||
mod rs255w245 {}
|
||||
#[rs(block = 255, data = 246)]
|
||||
mod rs255w246 {}
|
||||
#[rs(block = 255, data = 247)]
|
||||
mod rs255w247 {}
|
||||
#[rs(block = 255, data = 248)]
|
||||
mod rs255w248 {}
|
||||
#[rs(block = 255, data = 249)]
|
||||
mod rs255w249 {}
|
||||
#[rs(block = 255, data = 250)]
|
||||
mod rs255w250 {}
|
||||
#[rs(block = 255, data = 251)]
|
||||
mod rs255w251 {}
|
||||
#[rs(block = 255, data = 252)]
|
||||
mod rs255w252 {}
|
||||
#[rs(block = 255, data = 253)]
|
||||
mod rs255w253 {}
|
||||
|
||||
pub static FN_ENCODE: phf::Map<u8, fn(&mut [u8])> = phf_map! {
|
||||
231u8 => rs255w231::encode,
|
||||
232u8 => rs255w232::encode,
|
||||
233u8 => rs255w233::encode,
|
||||
234u8 => rs255w234::encode,
|
||||
235u8 => rs255w235::encode,
|
||||
236u8 => rs255w236::encode,
|
||||
237u8 => rs255w237::encode,
|
||||
238u8 => rs255w238::encode,
|
||||
239u8 => rs255w239::encode,
|
||||
240u8 => rs255w240::encode,
|
||||
241u8 => rs255w241::encode,
|
||||
242u8 => rs255w242::encode,
|
||||
243u8 => rs255w243::encode,
|
||||
244u8 => rs255w244::encode,
|
||||
245u8 => rs255w245::encode,
|
||||
246u8 => rs255w246::encode,
|
||||
247u8 => rs255w247::encode,
|
||||
248u8 => rs255w248::encode,
|
||||
249u8 => rs255w249::encode,
|
||||
250u8 => rs255w250::encode,
|
||||
251u8 => rs255w251::encode,
|
||||
252u8 => rs255w252::encode,
|
||||
253u8 => rs255w253::encode,
|
||||
};
|
||||
|
||||
pub static FN_IS_CORRECT: phf::Map<u8, fn(&[u8]) -> bool> = phf_map! {
|
||||
231u8 => rs255w231::is_correct,
|
||||
232u8 => rs255w232::is_correct,
|
||||
233u8 => rs255w233::is_correct,
|
||||
234u8 => rs255w234::is_correct,
|
||||
235u8 => rs255w235::is_correct,
|
||||
236u8 => rs255w236::is_correct,
|
||||
237u8 => rs255w237::is_correct,
|
||||
238u8 => rs255w238::is_correct,
|
||||
239u8 => rs255w239::is_correct,
|
||||
240u8 => rs255w240::is_correct,
|
||||
241u8 => rs255w241::is_correct,
|
||||
242u8 => rs255w242::is_correct,
|
||||
243u8 => rs255w243::is_correct,
|
||||
244u8 => rs255w244::is_correct,
|
||||
245u8 => rs255w245::is_correct,
|
||||
246u8 => rs255w246::is_correct,
|
||||
247u8 => rs255w247::is_correct,
|
||||
248u8 => rs255w248::is_correct,
|
||||
249u8 => rs255w249::is_correct,
|
||||
250u8 => rs255w250::is_correct,
|
||||
251u8 => rs255w251::is_correct,
|
||||
252u8 => rs255w252::is_correct,
|
||||
253u8 => rs255w253::is_correct,
|
||||
};
|
||||
|
||||
// Each one of these has its own error type, but the functions can only fail one
|
||||
// way (too many corrupt bytes), so just throw away the error and return an
|
||||
// Option instead.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub static FN_CORRECT_ERRORS: phf::Map<u8, fn(&mut [u8]) -> Option<usize>> = phf_map! {
|
||||
231u8 => |data: &mut [u8]| rs255w231::correct_errors(data).ok(),
|
||||
232u8 => |data: &mut [u8]| rs255w232::correct_errors(data).ok(),
|
||||
233u8 => |data: &mut [u8]| rs255w233::correct_errors(data).ok(),
|
||||
234u8 => |data: &mut [u8]| rs255w234::correct_errors(data).ok(),
|
||||
235u8 => |data: &mut [u8]| rs255w235::correct_errors(data).ok(),
|
||||
236u8 => |data: &mut [u8]| rs255w236::correct_errors(data).ok(),
|
||||
237u8 => |data: &mut [u8]| rs255w237::correct_errors(data).ok(),
|
||||
238u8 => |data: &mut [u8]| rs255w238::correct_errors(data).ok(),
|
||||
239u8 => |data: &mut [u8]| rs255w239::correct_errors(data).ok(),
|
||||
240u8 => |data: &mut [u8]| rs255w240::correct_errors(data).ok(),
|
||||
241u8 => |data: &mut [u8]| rs255w241::correct_errors(data).ok(),
|
||||
242u8 => |data: &mut [u8]| rs255w242::correct_errors(data).ok(),
|
||||
243u8 => |data: &mut [u8]| rs255w243::correct_errors(data).ok(),
|
||||
244u8 => |data: &mut [u8]| rs255w244::correct_errors(data).ok(),
|
||||
245u8 => |data: &mut [u8]| rs255w245::correct_errors(data).ok(),
|
||||
246u8 => |data: &mut [u8]| rs255w246::correct_errors(data).ok(),
|
||||
247u8 => |data: &mut [u8]| rs255w247::correct_errors(data).ok(),
|
||||
248u8 => |data: &mut [u8]| rs255w248::correct_errors(data).ok(),
|
||||
249u8 => |data: &mut [u8]| rs255w249::correct_errors(data).ok(),
|
||||
250u8 => |data: &mut [u8]| rs255w250::correct_errors(data).ok(),
|
||||
251u8 => |data: &mut [u8]| rs255w251::correct_errors(data).ok(),
|
||||
252u8 => |data: &mut [u8]| rs255w252::correct_errors(data).ok(),
|
||||
253u8 => |data: &mut [u8]| rs255w253::correct_errors(data).ok(),
|
||||
};
|
||||
+5
-5
@@ -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
|
||||
@@ -13,10 +11,12 @@
|
||||
// We use pb-rs' nostd mode. See build.rs.
|
||||
extern crate alloc;
|
||||
|
||||
pub mod boot;
|
||||
pub mod cli;
|
||||
pub mod crypto;
|
||||
pub mod escape;
|
||||
pub mod format;
|
||||
pub mod octal;
|
||||
pub mod patch;
|
||||
pub mod protobuf;
|
||||
pub mod stream;
|
||||
pub mod util;
|
||||
|
||||
+23
-10
@@ -1,16 +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::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::error;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
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));
|
||||
{
|
||||
@@ -22,5 +25,15 @@ fn main() -> Result<()> {
|
||||
.expect("Failed to set signal handler");
|
||||
}
|
||||
|
||||
avbroot::cli::args::main(&cancel_signal)
|
||||
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
|
||||
error!("{e:?}");
|
||||
} else {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
fmt::{self, Octal},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use num_traits::{Num, PrimInt};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: PrimInt + Octal,
|
||||
{
|
||||
serializer.serialize_str(&format!("{data:o}"))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
{
|
||||
struct OctalStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
|
||||
where
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "a string containing an octal number")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
T::from_str_radix(data, 8).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(OctalStrVisitor(PhantomData))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod boot;
|
||||
pub mod otacert;
|
||||
pub mod system;
|
||||
@@ -0,0 +1,148 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use x509_cert::{der::asn1::BitString, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
|
||||
|
||||
use crate::{crypto, format::ota};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("New otacerts.zip is too small to pad to {0} bytes")]
|
||||
ZipTooSmall(usize),
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Pad a non-zip64 zip file to the specified size by adding null bytes to the
|
||||
/// archive comment field.
|
||||
pub fn pad_zip(data: &mut Vec<u8>, size: usize) -> Result<()> {
|
||||
match size.cmp(&data.len()) {
|
||||
Ordering::Equal => Ok(()),
|
||||
Ordering::Less => Err(Error::ZipTooLarge(size)),
|
||||
Ordering::Greater => {
|
||||
let padding = size - data.len();
|
||||
|
||||
if data.len() < 22
|
||||
|| &data[data.len() - 22..][..4] != ota::ZIP_EOCD_MAGIC
|
||||
|| padding > usize::from(u16::MAX)
|
||||
{
|
||||
return Err(Error::ZipTooSmall(size));
|
||||
}
|
||||
|
||||
// Rewrite the comment size and pad with null bytes.
|
||||
data.pop();
|
||||
data.pop();
|
||||
data.extend((padding as u16).to_le_bytes());
|
||||
data.resize(size, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Android uses X.509 as nothing more than a file format to transport RSA
|
||||
/// public keys. This is true for both the framework's RecoverySystem and
|
||||
/// recovery's otautil/verifier.cpp. The only fields that must exist are
|
||||
/// the public key and the signature algorithm. The rest can be removed with
|
||||
/// no side effects whatsoever.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct OtaCertBuildFlags: u8 {
|
||||
const COMPRESS_DEFLATE = 1 << 0;
|
||||
const REMOVE_SIGNATURE = 1 << 1;
|
||||
const REMOVE_EXTENSIONS = 1 << 2;
|
||||
const REMOVE_ISSUER = 1 << 3;
|
||||
const REMOVE_SUBJECT = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an `otacerts.zip` file containing the specified certificate.
|
||||
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
|
||||
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Stored
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
} else {
|
||||
let mut modified = cert.clone();
|
||||
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
|
||||
modified.signature = BitString::from_bytes(&[])?;
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
|
||||
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
|
||||
extensions.clear();
|
||||
}
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
|
||||
modified.tbs_certificate.issuer.0.clear();
|
||||
modified.tbs_certificate.issuer_unique_id = None;
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SUBJECT) {
|
||||
modified.tbs_certificate.subject.0.clear();
|
||||
modified.tbs_certificate.subject_unique_id = None;
|
||||
}
|
||||
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
/// Create an `otacerts.zip` file padded to the specified size.
|
||||
///
|
||||
/// This will incrementally remove unneeded components from the certificate to
|
||||
/// meet the size limit if needed.
|
||||
pub fn create_zip_with_size(cert: &Certificate, size: usize) -> Result<Vec<u8>> {
|
||||
let mut flags = OtaCertBuildFlags::empty();
|
||||
|
||||
for additional_flag in [
|
||||
OtaCertBuildFlags::empty(),
|
||||
OtaCertBuildFlags::COMPRESS_DEFLATE,
|
||||
OtaCertBuildFlags::REMOVE_SIGNATURE,
|
||||
OtaCertBuildFlags::REMOVE_EXTENSIONS,
|
||||
OtaCertBuildFlags::REMOVE_ISSUER,
|
||||
OtaCertBuildFlags::REMOVE_SUBJECT,
|
||||
] {
|
||||
flags |= additional_flag;
|
||||
|
||||
trace!("Attempting to create {size} byte otacerts.zip: {flags:?}");
|
||||
|
||||
let mut data = create_zip(cert, flags)?;
|
||||
if data.len() <= size {
|
||||
trace!("Padding {} byte otacerts.zip to {size}", data.len());
|
||||
|
||||
pad_zip(&mut data, size)?;
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::ZipTooLarge(size))
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, SeekFrom},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, Span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer},
|
||||
ota,
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Old otacerts.zip not found in image")]
|
||||
OldZipNotFound,
|
||||
#[error("Image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash tree descriptor found in vbmeta header")]
|
||||
NoHashTreeDescriptor,
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Find the bounds of a non-zip64 zip starting from the EOCD magic offset.
|
||||
fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
|
||||
let eocd = &data[eocd_offset..];
|
||||
if eocd.len() < 22 {
|
||||
trace!("Buffer is too small to contain EOCD");
|
||||
return None;
|
||||
}
|
||||
|
||||
let cd_size = u32::from_le_bytes(eocd[12..16].try_into().unwrap()) as usize;
|
||||
let cd_offset = u32::from_le_bytes(eocd[16..20].try_into().unwrap()) as usize;
|
||||
let comment_size = usize::from(u16::from_le_bytes(eocd[20..22].try_into().unwrap()));
|
||||
|
||||
let start = eocd_offset.checked_sub(cd_size)?.checked_sub(cd_offset)?;
|
||||
let end = eocd_offset.checked_add(22)?.checked_add(comment_size)?;
|
||||
if end > data.len() {
|
||||
trace!("End of zip is out of bounds");
|
||||
return None;
|
||||
}
|
||||
|
||||
trace!("Found zip bounds: {:?}", start..end);
|
||||
|
||||
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
|
||||
let mut zip_reader = ZipArchive::new(reader).ok()?;
|
||||
|
||||
if zip_reader.is_empty() {
|
||||
// otacerts.zip files contain at least one cert.
|
||||
trace!("Zip is empty");
|
||||
return None;
|
||||
}
|
||||
|
||||
for index in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index_raw(index).ok()?;
|
||||
|
||||
if !entry.name().ends_with(".x509.pem") {
|
||||
// otacerts.zip files only contain files named this way.
|
||||
trace!("Excluded due to invalid name: {:?}", entry.name());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Found otacerts.zip candidate");
|
||||
|
||||
// There's one or more entries and every one is named *.x509.pem.
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
/// Replace `otacerts.zip` with a new one containing the new certificate, but
|
||||
/// padded to the same size. If the new zip is too large, the certificate will
|
||||
/// be modified to remove unnecessary components until it fits. All operations
|
||||
/// run in parallel where possible. The input and output must refer to the same
|
||||
/// file and will be reopened from multiple threads.
|
||||
///
|
||||
/// Returns two sorted and non-overlapping lists of byte ranges that were
|
||||
/// modified. The first list are the byte regions within the filesystem data
|
||||
/// that contained otacerts.zip. The second list is the list of byte regions
|
||||
/// outside of the filesyste, like the hash tree, FEC data, and AVB metadata.
|
||||
///
|
||||
/// If [`Error::OldZipNotFound`] is returned, the output will not have been
|
||||
/// modified.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn patch_system_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
certificate: &Certificate,
|
||||
key: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
|
||||
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
|
||||
// This ensures that the block containing otacerts.zip's data won't cross
|
||||
// chunk boundaries.
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
let num_chunks = 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)?;
|
||||
|
||||
let offset = chunk * CHUNK_SIZE;
|
||||
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut ranges = Vec::<Range<u64>>::new();
|
||||
|
||||
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
|
||||
let _span = debug_span!(parent: &parent_span, "otacerts", offset, eocd_offset_rel)
|
||||
.entered();
|
||||
|
||||
let Some(bounds_rel) = find_zip_bounds(&buf, eocd_offset_rel) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let zip_size = bounds_rel.end - bounds_rel.start;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
|
||||
|
||||
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(bounds.start))?;
|
||||
writer.write_all(&new_zip)?;
|
||||
|
||||
ranges.push(bounds);
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
})
|
||||
.try_reduce(Vec::new, |mut result, item| {
|
||||
result.extend(item);
|
||||
Ok(result)
|
||||
})?;
|
||||
|
||||
if modified_ranges.is_empty() {
|
||||
return Err(Error::OldZipNotFound);
|
||||
}
|
||||
|
||||
let update_ranges = if descriptor.hash_algorithm == "sha1" {
|
||||
// Promote to a secure algorithm. SHA1 is allowed for verification only.
|
||||
// The entire hash tree and FEC data will need to be recomputed.
|
||||
let new_algorithm = "sha256".to_owned();
|
||||
|
||||
debug!(
|
||||
"Changing insecure hash algorithm {} to {new_algorithm}",
|
||||
descriptor.hash_algorithm,
|
||||
);
|
||||
|
||||
descriptor.hash_algorithm = new_algorithm;
|
||||
None
|
||||
} else {
|
||||
// Only need to update the hash tree and FEC data corresponding to the
|
||||
// modified regions.
|
||||
Some(modified_ranges.as_slice())
|
||||
};
|
||||
|
||||
descriptor.update(input, output, update_ranges, cancel_signal)?;
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
debug!("Signing system image");
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed()?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
// The hash tree, FEC data, and AVB regions will have been modified.
|
||||
let hash_tree_end = descriptor
|
||||
.tree_offset
|
||||
.checked_add(descriptor.tree_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
|
||||
let fec_data_end = descriptor
|
||||
.fec_offset
|
||||
.checked_add(descriptor.fec_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
|
||||
let header_end = footer
|
||||
.vbmeta_offset
|
||||
.checked_add(footer.vbmeta_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
|
||||
let footer_start = image_size - Footer::SIZE as u64;
|
||||
|
||||
let other_ranges = util::merge_overlapping(&[
|
||||
descriptor.tree_offset..hash_tree_end,
|
||||
descriptor.fec_offset..fec_data_end,
|
||||
footer.vbmeta_offset..header_end,
|
||||
footer_start..image_size,
|
||||
]);
|
||||
|
||||
Ok((modified_ranges, other_ranges))
|
||||
}
|
||||
+11
-1
@@ -1 +1,11 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs"));
|
||||
pub mod build {
|
||||
pub mod tools {
|
||||
pub mod releasetools {
|
||||
include!(concat!(env!("OUT_DIR"), "/build.tools.releasetools.rs"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod chromeos_update_engine {
|
||||
include!(concat!(env!("OUT_DIR"), "/chromeos_update_engine.rs"));
|
||||
}
|
||||
|
||||
+115
-94
@@ -1,21 +1,20 @@
|
||||
/*
|
||||
* 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, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
use crate::util::{self, EscapedString};
|
||||
use crate::util;
|
||||
|
||||
/// A trait for seekable readers. This is only needed because `dyn Read + Seek`
|
||||
/// is not a valid construct in Rust yet.
|
||||
@@ -29,6 +28,28 @@ pub trait WriteSeek: Write + Seek {}
|
||||
|
||||
impl<W: Write + Seek> WriteSeek for W {}
|
||||
|
||||
/// A trait for seekable and reopenable readers.
|
||||
pub trait ReadSeekReopen: ReadSeek {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>>;
|
||||
}
|
||||
|
||||
impl<R: ReadSeek + Reopen + 'static> ReadSeekReopen for R {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>> {
|
||||
Ok(Box::new(self.reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for seekable and reopenable writers.
|
||||
pub trait WriteSeekReopen: WriteSeek {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>>;
|
||||
}
|
||||
|
||||
impl<W: WriteSeek + Reopen + 'static> WriteSeekReopen for W {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>> {
|
||||
Ok(Box::new(self.reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Common function for reading a structure from a reader.
|
||||
pub trait FromReader<R: Read>: Sized {
|
||||
type Error;
|
||||
@@ -118,7 +139,7 @@ impl<R: Read> ReadStringExt for R {
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {}: {e}", EscapedString::new(e.as_bytes())),
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -138,7 +159,7 @@ impl<R: Read> ReadStringExt for R {
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {}: {e}", EscapedString::new(e.as_bytes())),
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -167,9 +188,28 @@ impl<W: Write> WriteStringExt for W {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for file-like types to reopen themselves.
|
||||
pub trait Reopen: Sized {
|
||||
/// Open a new handle to the same file. The new handle is independently
|
||||
/// seekable and the file offset is initially set to 0.
|
||||
fn reopen(&self) -> io::Result<Self>;
|
||||
}
|
||||
|
||||
impl<R: Read + Reopen> Reopen for BufReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufReader::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()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
@@ -207,7 +247,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,
|
||||
}
|
||||
@@ -248,7 +288,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,
|
||||
}
|
||||
@@ -272,7 +312,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,
|
||||
}
|
||||
@@ -300,7 +340,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,
|
||||
@@ -324,6 +364,14 @@ impl<R: Read + Seek> SectionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek + Reopen> Reopen for SectionReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
let inner = self.inner.reopen()?;
|
||||
|
||||
Self::new(inner, self.start, self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for SectionReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let to_read = self.size.saturating_sub(self.pos).min(buf.len() as u64) as usize;
|
||||
@@ -366,41 +414,10 @@ impl<R: Read + Seek> Seek for SectionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer wrapper that seeks instead of writing when a write buffer consists
|
||||
/// solely of zeros.
|
||||
/// 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.
|
||||
#[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 cloned instances uses
|
||||
/// the same underlying kernel file descriptor, but a new userspace file offset.
|
||||
#[derive(Clone)]
|
||||
pub struct PSeekFile {
|
||||
// The lock is needed because flush() takes a `&mut self`.
|
||||
file: Arc<RwLock<File>>,
|
||||
@@ -449,6 +466,15 @@ impl PSeekFile {
|
||||
}
|
||||
}
|
||||
|
||||
impl Reopen for PSeekFile {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
file: self.file.clone(),
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for PSeekFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let n = self.read_at(buf)?;
|
||||
@@ -509,17 +535,26 @@ impl Seek for PSeekFile {
|
||||
/// readers into different parts of the same [`SharedCursor`] writer and the
|
||||
/// read operation is significantly more expensive than the write operation (eg.
|
||||
/// due to decompression).
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Default)]
|
||||
pub struct SharedCursor {
|
||||
inner: Arc<Mutex<Cursor<Vec<u8>>>>,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl SharedCursor {
|
||||
pub fn clone_rewind(&self) -> Self {
|
||||
let mut new = self.clone();
|
||||
new.offset = 0;
|
||||
new
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Reopen for SharedCursor {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: self.inner.clone(),
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +595,21 @@ impl Seek for SharedCursor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an I/O error with the [`io::ErrorKind::Interrupted`] type if
|
||||
/// `cancel_signal` is true. This should be called frequently in I/O loops for
|
||||
/// cancellation to be responsive.
|
||||
#[inline]
|
||||
pub fn check_cancel(cancel_signal: &AtomicBool) -> io::Result<()> {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Received cancel signal",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy exactly `size` bytes from `reader` to `writer`, invoking `inspect`
|
||||
/// after every buffer read iteration. If either `reader` or `writer` reaches
|
||||
/// EOF before `size` bytes are copied, an error is returned. The operation is
|
||||
@@ -569,17 +619,12 @@ pub fn copy_n_inspect(
|
||||
mut writer: impl Write,
|
||||
mut size: u64,
|
||||
mut inspect: impl FnMut(&[u8]),
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
let mut buf = [0u8; 16384];
|
||||
|
||||
while size > 0 {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Received cancel signal",
|
||||
));
|
||||
}
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
let to_read = size.min(buf.len() as u64) as usize;
|
||||
reader.read_exact(&mut buf[..to_read])?;
|
||||
@@ -599,7 +644,7 @@ pub fn copy_n(
|
||||
reader: impl Read,
|
||||
writer: impl Write,
|
||||
size: u64,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
copy_n_inspect(reader, writer, size, |_| {}, cancel_signal)
|
||||
}
|
||||
@@ -610,18 +655,13 @@ pub fn copy_n(
|
||||
pub fn copy(
|
||||
mut reader: impl Read,
|
||||
mut writer: impl Write,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<u64> {
|
||||
let mut buf = [0u8; 16384];
|
||||
let mut copied = 0;
|
||||
|
||||
loop {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Received cancel signal",
|
||||
));
|
||||
}
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
let n = reader.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
@@ -640,18 +680,14 @@ pub fn copy(
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use ring::digest::Context;
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, HolePunchingWriter,
|
||||
PSeekFile, ReadDiscardExt, ReadStringExt, SectionReader, SharedCursor, WriteStringExt,
|
||||
WriteZerosExt,
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
|
||||
ReadStringExt, Reopen, SectionReader, SharedCursor, WriteStringExt, WriteZerosExt,
|
||||
};
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
@@ -811,27 +847,12 @@ 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();
|
||||
let mut a = PSeekFile::new(raw_file);
|
||||
let mut b = a.clone();
|
||||
let mut c = b.clone();
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
@@ -850,8 +871,8 @@ mod tests {
|
||||
#[test]
|
||||
fn shared_cursor() {
|
||||
let mut a = SharedCursor::default();
|
||||
let mut b = a.clone();
|
||||
let mut c = b.clone();
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
@@ -869,7 +890,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn copy() {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let mut reader = Cursor::new(b"foobar");
|
||||
let mut writer = Cursor::new([0u8; 6]);
|
||||
|
||||
|
||||
+108
-85
@@ -1,21 +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::fmt;
|
||||
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
|
||||
|
||||
use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer};
|
||||
use num_traits::PrimInt;
|
||||
|
||||
pub const ZEROS: [u8; 16384] = [0u8; 16384];
|
||||
|
||||
/// A small wrapper to format a number as a size in bytes.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct NumBytes(pub usize);
|
||||
pub struct NumBytes<T: PrimInt>(pub T);
|
||||
|
||||
impl fmt::Debug for NumBytes {
|
||||
impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.0 == 1 {
|
||||
if self.0 == T::one() {
|
||||
write!(f, "<{:?} byte>", self.0)
|
||||
} else {
|
||||
write!(f, "<{:?} bytes>", self.0)
|
||||
@@ -23,72 +21,6 @@ impl fmt::Debug for NumBytes {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a byte slice to format it as ASCII with invalid bytes
|
||||
/// escaped as `\x##`.
|
||||
#[derive(Clone)]
|
||||
pub struct EscapedString<T: AsRef<[u8]>> {
|
||||
inner: T,
|
||||
quoted: bool,
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> EscapedString<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
quoted: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_unquoted(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
quoted: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn get_ref(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> fmt::Debug for EscapedString<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let escaped: String = self
|
||||
.inner
|
||||
.as_ref()
|
||||
.iter()
|
||||
.flat_map(|b| b.escape_ascii())
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
if self.quoted {
|
||||
write!(f, "\"")?;
|
||||
}
|
||||
|
||||
write!(f, "{escaped}")?;
|
||||
|
||||
if self.quoted {
|
||||
write!(f, "\"")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> fmt::Display for EscapedString<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a byte slice is all zeros.
|
||||
pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
while !buf.is_empty() {
|
||||
@@ -103,16 +35,107 @@ pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Read a protobuf message with no leading size field.
|
||||
pub fn read_protobuf<'a, M: MessageRead<'a>>(data: &'a [u8]) -> quick_protobuf::Result<M> {
|
||||
let mut reader = BytesReader::from_bytes(data);
|
||||
M::from_reader(&mut reader, data)
|
||||
/// Get the non-empty parent of a path. If the path has no parent in the string,
|
||||
/// then `.` is returned. This does not perform any filesystem operations.
|
||||
pub fn parent_path(path: &Path) -> &Path {
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
Path::new(".")
|
||||
}
|
||||
|
||||
/// Write a protobuf message with no leading size field.
|
||||
pub fn write_protobuf<M: MessageWrite>(message: &M) -> quick_protobuf::Result<Vec<u8>> {
|
||||
let mut buf = Vec::with_capacity(message.get_size());
|
||||
let mut writer = Writer::new(&mut buf);
|
||||
message.write_message(&mut writer)?;
|
||||
Ok(buf)
|
||||
/// Sort and merge overlapping intervals.
|
||||
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
|
||||
where
|
||||
T: Ord + Clone + Copy,
|
||||
{
|
||||
let mut sections = sections.to_vec();
|
||||
sections.sort_by_key(|r| (r.start, r.end));
|
||||
|
||||
let mut result = Vec::<Range<T>>::new();
|
||||
|
||||
for section in sections {
|
||||
if section.start >= section.end {
|
||||
continue;
|
||||
} else if let Some(last) = result.last_mut() {
|
||||
if section.start <= last.end {
|
||||
last.end = last.end.max(section.end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(section);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Binary search to determine if the needle overlaps any of the ranges.
|
||||
pub fn ranges_overlaps<T>(ranges: &[Range<T>], needle: &Range<T>) -> bool
|
||||
where
|
||||
T: Ord,
|
||||
{
|
||||
if needle.start < needle.end {
|
||||
ranges
|
||||
.binary_search_by(|range| {
|
||||
if range.start > needle.end {
|
||||
Ordering::Greater
|
||||
} else if range.end <= needle.start {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary search to determine if any of the ranges contain the needle.
|
||||
pub fn ranges_contains<T>(ranges: &[Range<T>], needle: &T) -> bool
|
||||
where
|
||||
T: Ord,
|
||||
{
|
||||
ranges
|
||||
.binary_search_by(|range| {
|
||||
if range.start > *needle {
|
||||
Ordering::Greater
|
||||
} else if range.end <= *needle {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ranges_overlaps() {
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_contains() {
|
||||
assert_eq!(ranges_contains(&[0..4], &0), true);
|
||||
assert_eq!(ranges_contains(&[0..4], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
|
||||
}
|
||||
}
|
||||
|
||||
+433
-46
@@ -1,17 +1,27 @@
|
||||
/*
|
||||
* 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, Read, Seek, SeekFrom};
|
||||
use std::{
|
||||
io::{Cursor, Read, Seek, Write},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use pkcs8::DecodePrivateKey;
|
||||
use rsa::RsaPrivateKey;
|
||||
|
||||
use avbroot::{self, format::avb};
|
||||
use avbroot::{
|
||||
self,
|
||||
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",
|
||||
@@ -21,62 +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 = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: 4096,
|
||||
tree_offset: 0,
|
||||
tree_size: 2048,
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 1,
|
||||
fec_offset: 2048,
|
||||
fec_size: 2048,
|
||||
hash_algorithm: "sha512".to_owned(),
|
||||
partition_name: "hashtreed_partition".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(8),
|
||||
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(8),
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
Descriptor::Hash(HashDescriptor {
|
||||
image_size: 6,
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "hashed_partition".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(4),
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
Descriptor::KernelCmdline(KernelCmdlineDescriptor {
|
||||
flags: 1,
|
||||
cmdline: "foobar".to_owned(),
|
||||
}),
|
||||
Descriptor::ChainPartition(ChainPartitionDescriptor {
|
||||
rollback_index_location: 1,
|
||||
partition_name: "chained_partition".to_owned(),
|
||||
public_key: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(129),
|
||||
flags: 0xfedcba98,
|
||||
reserved: repeat_array(&[0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let (mut header, footer, _) = avb::load_image(reader).unwrap();
|
||||
assert_matches!(footer, None);
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
// 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.
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
avb::write_root_image(&mut writer, &header, 64).unwrap();
|
||||
let new_data = writer.into_inner();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
|
||||
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
|
||||
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
|
||||
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
|
||||
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
assert_matches!(new_footer, None);
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_image_size, data.len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_image() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vbmeta_appended.img",
|
||||
));
|
||||
let mut reader = Cursor::new(data);
|
||||
|
||||
let (mut header, footer, _) = avb::load_image(&mut reader).unwrap();
|
||||
let footer = footer.unwrap();
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
fn round_trip_appended_hash_image() {
|
||||
let image_size = 12288;
|
||||
let raw_data = b"foobar";
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::Hash(HashDescriptor {
|
||||
image_size: raw_data.len() as u64,
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "vbmeta_appended_hash".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: vec![], // autogenerated
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
original_image_size: 0, // autogenerated
|
||||
vbmeta_offset: 0, // autogenerated
|
||||
vbmeta_size: 0, // autogenerated
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Copy the partition data.
|
||||
let image_size = reader.seek(SeekFrom::End(0)).unwrap();
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
io::copy(&mut reader.take(footer.original_image_size), &mut writer).unwrap();
|
||||
// Write the raw partition data.
|
||||
writer.write_all(raw_data).unwrap();
|
||||
|
||||
// Write new vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &footer, image_size).unwrap();
|
||||
let new_data = writer.into_inner();
|
||||
// Regenerate the raw image digest.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(_) => panic!("Expected hash descriptor"),
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
writer.rewind().unwrap();
|
||||
d.update(&mut writer, &cancel_signal).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
// 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 vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
|
||||
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
|
||||
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
|
||||
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
|
||||
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let new_footer = new_footer.unwrap();
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, image_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
let image_size = 32768;
|
||||
let raw_data: [u8; 8192] = repeat_array(b"foobar");
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: raw_data.len() as u64,
|
||||
tree_offset: 0, // autogenerated
|
||||
tree_size: 0, // autogenerated
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 2,
|
||||
fec_offset: 0, // autogenerated
|
||||
fec_size: 0, // autogenerated
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "vbmeta_appended_hash_tree".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: vec![], // autogenerated
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
original_image_size: 0, // autogenerated
|
||||
vbmeta_offset: 0, // autogenerated
|
||||
vbmeta_size: 0, // autogenerated
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// 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, Some(image_size)).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(),
|
||||
[
|
||||
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
|
||||
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
|
||||
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
|
||||
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
|
||||
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let new_footer = new_footer.unwrap();
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, image_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
let raw_data: [u8; 8192] = repeat_array(b"foobar");
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: raw_data.len() as u64,
|
||||
tree_offset: 0, // autogenerated
|
||||
tree_size: 0, // autogenerated
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 2,
|
||||
fec_offset: 0, // autogenerated
|
||||
fec_size: 0, // autogenerated
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "vbmeta_appended_hash_tree".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: vec![], // autogenerated
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
original_image_size: 0, // autogenerated
|
||||
vbmeta_offset: 0, // autogenerated
|
||||
vbmeta_size: 0, // autogenerated
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Write the raw partition data.
|
||||
writer.write_all(&raw_data).unwrap();
|
||||
|
||||
// Generate and write the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.update(&writer, &writer, None, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Verify the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.verify(&writer, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Sign the header.
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
|
||||
let mut data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut data).unwrap();
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
|
||||
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
|
||||
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
|
||||
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
|
||||
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let new_footer = new_footer.unwrap();
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, 28672);
|
||||
}
|
||||
|
||||
+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};
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
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 = 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 mut cpio_reader = CpioReader::new(reader, false);
|
||||
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
while let Some(entry) = cpio_reader.next_entry().unwrap() {
|
||||
cpio_writer.start_entry(&entry).unwrap();
|
||||
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
io::copy(&mut cpio_reader, &mut cpio_writer).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let writer = cpio_writer.finish().unwrap();
|
||||
let new_data = writer.get_ref().as_slice();
|
||||
|
||||
if pad_to_block_size {
|
||||
assert!(new_data.starts_with(&data));
|
||||
assert!(util::is_zero(&new_data[data.len()..]));
|
||||
assert_eq!(new_data.len() % 512, 0);
|
||||
} else {
|
||||
assert_eq!(new_data, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
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,21 +1,45 @@
|
||||
[advisories]
|
||||
vulnerability = "deny"
|
||||
unmaintained = "deny"
|
||||
version = 2
|
||||
yanked = "deny"
|
||||
notice = "deny"
|
||||
ignore = [
|
||||
# https://rustsec.org/advisories/RUSTSEC-2023-0071
|
||||
#
|
||||
# This is a side-channel vulnerability where secrets can be leaked to an
|
||||
# attacker that is able to measure the timing of a large number of RSA
|
||||
# operations. As of 2023-12-03, there is no released version of the rsa
|
||||
# crate that contains a fix.
|
||||
#
|
||||
# For avbroot specifically, this vulnerability is not too critical for a
|
||||
# couple reasons:
|
||||
#
|
||||
# 1. avbroot performs RSA signing only at the end of lengthy processes
|
||||
# that involve a lot of disk I/O. It's very expensive to run avbroot
|
||||
# the millions of times needed to capture a sufficient amount of timing
|
||||
# data.
|
||||
# 2. During a single run of avbroot, it will only perform RSA signing a
|
||||
# handful of times. To get sufficient measurements, the attacker would
|
||||
# need to rerun avbroot. If they are able to rerun avbroot, then they
|
||||
# are also able to just read and steal the private key directly.
|
||||
#
|
||||
# avbroot has no network capabilities, so this is not inherently remotely
|
||||
# exploitable.
|
||||
"RUSTSEC-2023-0071",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
unlicensed = "deny"
|
||||
version = 2
|
||||
include-dev = true
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
copyleft = "allow"
|
||||
default = "deny"
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
@@ -26,15 +50,25 @@ license-files = [
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
multiple-versions-include-dev = true
|
||||
deny = [
|
||||
# https://github.com/serde-rs/serde/issues/2538
|
||||
{ name = "serde_derive", version = ">=1.0.172,<1.0.184" },
|
||||
]
|
||||
|
||||
[bans.build]
|
||||
executables = "deny"
|
||||
include-dependencies = true
|
||||
include-workspace = true
|
||||
bypass = [
|
||||
# Copies of unmodified crashwrangler objects for old macOS versions.
|
||||
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/bzip2-rs",
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
"https://github.com/jongiddy/bzip2-rs",
|
||||
]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/files/
|
||||
+10
-5
@@ -14,16 +14,21 @@ avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
reqwest = { version = "0.11.20", features = ["stream"] }
|
||||
ring = "0.16.20"
|
||||
ring = "0.17.0"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
tokio = { version = "1.32.0", features = ["signal", "rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.14"
|
||||
toml_edit = { version = "0.19.14", features = ["serde"] }
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = "0.2.5"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
static = ["avbroot/static"]
|
||||
|
||||
+12
-50
@@ -1,64 +1,26 @@
|
||||
# End-to-end tests
|
||||
|
||||
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
|
||||
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some profiles for generating mock OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
|
||||
|
||||
For each image listed in the config, the test process will:
|
||||
For each profile listed in the config, the test process will:
|
||||
|
||||
1. Download the OTA if it doesn't already exist in `./files/<device>/` (or the workdir specified by `-w`)
|
||||
2. Verify the OTA checksum
|
||||
3. Run avbroot against the OTA using `--magisk`
|
||||
4. Extract the AVB-related partitions from the patched OTA and verify their checksums
|
||||
5. Verify the patched OTA checksum
|
||||
1. Generate a mock OTA based on the specification
|
||||
2. Verify tha original OTA checksum
|
||||
3. Run avbroot against the OTA using `--magisk` (with a mock Magisk APK)
|
||||
4. Verify the patched OTA checksum
|
||||
5. Extract the AVB-related partitions from the patched OTA
|
||||
6. Run avbroot against the OTA again using `--prepatched`
|
||||
7. Verify the patched OTA checksum again
|
||||
|
||||
For more efficient CI testing, the tests can operate on "stripped" OTAs. A stripped OTA is identical to the full OTA, except that partitions in `payload.bin` unrelated to AVB are zeroed out. This reduces the download size and disk space requirements by a couple orders of magnitude. **A stripped OTA is NOT bootable and should never be flashed on a real device.**
|
||||
The default profiles shipped with the project mimic how various stock OTAs for Pixel devices are built. The generated mock OTAs have valid signatures and data structures for all components, but without any actual data where possible. For example, most files in the ramdisks are empty files. To ensure the mock OTAs cannot be mistakenly installed on a real device, the OTA metadata lists a fake device name in the preconditions section.
|
||||
|
||||
## Running the tests
|
||||
|
||||
To test against the device OTA images listed in [`e2e.toml`](./e2e.toml), run:
|
||||
To test against the profiles listed in [`e2e.toml`](./e2e.toml), run:
|
||||
|
||||
```bash
|
||||
# To test all device OTAs
|
||||
# To test all profiles
|
||||
cargo run --release -- test -a
|
||||
# Or to test against specific device OTAs
|
||||
cargo run --release -- test -d cheetah -d bluejay
|
||||
# Or to test against specific profiles
|
||||
cargo run --release -- test -p pixel_v4_gki -p pixel_v4_non_gki
|
||||
```
|
||||
|
||||
To test against stripped OTAs (smaller download, but not bootable), pass in `--stripped`.
|
||||
|
||||
## Downloading a device image
|
||||
|
||||
To download a full OTA image, run:
|
||||
|
||||
```bash
|
||||
cargo run --release -- download -d <device>
|
||||
```
|
||||
|
||||
This normally happens automatically when running the `test` subcommand. To download the stripped OTA image instead, pass in `--stripped`.
|
||||
|
||||
If the image file does not already exist, then it will be downloaded and the checksums will be validated. If the download is interrupted, it will automatically resume when the command is rerun. If the file is already downloaded, the command is effectively a no-op unless `--revalidate` is passed in to revalidate the image checksums.
|
||||
|
||||
## Adding a new device image
|
||||
|
||||
To add a new device image to the testing configuration, run:
|
||||
|
||||
```bash
|
||||
cargo run --release -- add -d <device> -u <full OTA URL> -H <expected checksum>
|
||||
```
|
||||
|
||||
If the OS vendor does not provide a SHA-256 checksum, omit `-H` and the program will compute the checksum from the downloaded data.
|
||||
|
||||
This process will download the full OTA, strip it, patch the full OTA, patch the stripped OTA, extract the AVB partitions, and write all of the checksums to [`e2e.toml`](./e2e.toml).
|
||||
|
||||
The process for updating an existing device config is exactly the same as adding a new one.
|
||||
|
||||
## Stripping a full OTA
|
||||
|
||||
To convert a full OTA to the stripped form, run:
|
||||
|
||||
```bash
|
||||
cargo run --release -- strip -i <input zip> -o <output zip>
|
||||
```
|
||||
|
||||
This normally happens automatically as a part of adding a new device image.
|
||||
|
||||
+163
-103
@@ -1,116 +1,176 @@
|
||||
[magisk]
|
||||
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.0/Magisk-v26.0.apk"
|
||||
"hash" = "9e14d3d3ca1f1a2765f8ca215ebbf35ea5fd2896fb147eea581fcaa3b4e77d25"
|
||||
# Metadata used when generating OTAs. These values don't affect behavior at all.
|
||||
[ota_info]
|
||||
# Make sure generated OTAs aren't flashable on real devices.
|
||||
device = "avbroot_fake_device"
|
||||
fingerprint = "avbroot/avbroot_fake_device:14/UQ1A.240101.000/12345678:user/release-keys"
|
||||
build_number = "UQ1A.240101.000"
|
||||
incremental_version = "12345678"
|
||||
android_version = "14"
|
||||
sdk_version = "34"
|
||||
security_patch_level = "2024-01-01"
|
||||
|
||||
# Google Pixel 7 Pro
|
||||
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
|
||||
[device.cheetah]
|
||||
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq2a.230305.008.c1-6ac5ff2e.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 151715 },
|
||||
{ start = 21683150, end = 23179365 },
|
||||
{ start = 2043499985, end = 2043508325 },
|
||||
{ start = 2315331822, end = 2333060126 },
|
||||
{ start = 2344084950, end = 2344090066 },
|
||||
]
|
||||
hash.original.full = "6ac5ff2e14dc16755ea4ea30e6dbe25103b889a36a465194ef943bd0d665b91c"
|
||||
hash.original.stripped = "549522015f0369a3b89385f532ab62235b47c2c39540bd0adaaf6acc81fdda94"
|
||||
hash.patched.full = "b380720852e2a1e994bcf38064f577bac68b18e799cf8166cb6a7edb8a661cb4"
|
||||
hash.patched.stripped = "560cdf4d7b25fb5bc650f96ae5a3c7593ac229a364bb39887287cfb734f6b377"
|
||||
hash.avb_images."init_boot.img" = "fac9305ce22b897fbfb193968d5346f4c70f6c18c3060bb106226f134ce5f433"
|
||||
hash.avb_images."vbmeta.img" = "0b3d719b751dd43bbec95d02b1bf57b5dae62e42a52502895892a207b773e77d"
|
||||
hash.avb_images."vbmeta_system.img" = "cf8c77dcf0a4474d49b5bdc2a44bdb3646464d5212fbe12aa5d3c5f531742f4f"
|
||||
hash.avb_images."vbmeta_vendor.img" = "660d8f61acd95a4f8ad416b4cbe126e9c039706462b4236ad723953c72ac49a8"
|
||||
hash.avb_images."vendor_boot.img" = "c788d4d8eb7926ad1bfa2a9c000343c3c73cedc580449f5270a711feb620f033"
|
||||
|
||||
[profile.pixel_v4_gki]
|
||||
vabc_algo = "Lz4"
|
||||
|
||||
[profile.pixel_v4_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v4"
|
||||
data.kernel = true
|
||||
|
||||
[profile.pixel_v4_gki.partitions.init_boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v4"
|
||||
data.ramdisks = [["init", "first_stage"]]
|
||||
|
||||
[profile.pixel_v4_gki.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v4_gki.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "init_boot", "vendor_boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v4_gki.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v4_gki.partitions.vendor_boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "vendor_v4"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
|
||||
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
|
||||
patched = "e7b4609ba7a23609211dcae143bc43f091f286fbbb3a9301c02ee25614d35deb"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
[device.bluejay]
|
||||
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq2a.230305.008.e1-915f9087.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 140787 },
|
||||
{ start = 1060207, end = 21612852 },
|
||||
{ start = 1886150700, end = 1886158844 },
|
||||
{ start = 2069112987, end = 2092260102 },
|
||||
{ start = 2098778558, end = 2098783674 },
|
||||
]
|
||||
hash.original.full = "915f9087b627b6961be9bb447dc63a7a1083b536753a78715e98641eaeb9c9d1"
|
||||
hash.original.stripped = "a3ee5b6e39e687665c31790118ab9f47715b0b8285ae9847dbf81307f963db14"
|
||||
hash.patched.full = "bfa7b26d90bdc889a7a199439e1564b219e5e2dbfddc1657bc1c6b73229be67e"
|
||||
hash.patched.stripped = "4e56ee4a8554f2b08ffc2f1470ad60b9b63d2b6fb469ed1f7c4b1204bbf8ad7d"
|
||||
hash.avb_images."boot.img" = "a1a705092e7034d20b83c94d78291418e13c343b1573c1b11e0fc884fc00ae62"
|
||||
hash.avb_images."vbmeta.img" = "3c123705be57ab142d2b43beef9b123eaad129df17a523b59b1d63b9122d28b0"
|
||||
hash.avb_images."vbmeta_system.img" = "285b83e4290f3257dc3678f0c3191794830bb2d72fb0969b69fc8f09d7ddff12"
|
||||
hash.avb_images."vbmeta_vendor.img" = "981f736586b91a9f4c93c4208a0d191a35ff15118c6fa505d755ee7fda8b2477"
|
||||
hash.avb_images."vendor_boot.img" = "ceffcb4fdb33aa3bb2c70621060acd5af612206b21cc413d5c2d39fee25144ab"
|
||||
|
||||
[profile.pixel_v4_non_gki]
|
||||
vabc_algo = "Lz4"
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v4"
|
||||
data.kernel = true
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.vendor_boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "vendor_v4"
|
||||
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_streaming]
|
||||
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
|
||||
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_seekable]
|
||||
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
|
||||
patched = "3456b14e014cf565a808a9e834d9105a23539f07b2c460db19c9384aadbc3b93"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
[device.bramble]
|
||||
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq2a.230305.008.c1-a925dd09.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 140531 },
|
||||
{ start = 496187, end = 11655082 },
|
||||
{ start = 1650283561, end = 1650287993 },
|
||||
{ start = 1884739919, end = 1908081011 },
|
||||
{ start = 1910894027, end = 1910899144 },
|
||||
]
|
||||
hash.original.full = "a925dd09c8d613d46cf72677c16f4fadee18bc21734d57047c6ccf31f672507b"
|
||||
hash.original.stripped = "79322b0b417359e8f072032de676d7e5bd2715a3b3554c48ed5cc9e9a25c6866"
|
||||
hash.patched.full = "cf79cd60acd3635f5085d8bb411d4a8dc4d7e62440f62a14e7ea12f5a9b7cd8a"
|
||||
hash.patched.stripped = "b684a78fe08014f1633e74d1f380e137823ba77a47ddf428bc649364f5548b75"
|
||||
hash.avb_images."boot.img" = "2bbf2c6d2f82d454426b26ac3b4887b26ca0591b458e6cf137207ecbb8f7649d"
|
||||
hash.avb_images."vbmeta.img" = "ab3b2487671b3fc28e163898621d6c068164d335b6ddd9b94e4fde9471c95d66"
|
||||
hash.avb_images."vbmeta_system.img" = "2fcd52d7462916a8510bbb07f2f5a14200afe2de97568396fe75e04c5c283152"
|
||||
hash.avb_images."vendor_boot.img" = "e27f157c4ebf4e958165997a4b87d4de1cfc34f4dea423a153327a27b053cac7"
|
||||
|
||||
[profile.pixel_v3]
|
||||
vabc_algo = "Lz4"
|
||||
|
||||
[profile.pixel_v3.partitions.boot]
|
||||
avb.signed = true
|
||||
data.type = "boot"
|
||||
data.version = "v3"
|
||||
data.kernel = true
|
||||
data.ramdisks = [["init"]]
|
||||
|
||||
[profile.pixel_v3.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v3.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v3.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v3.partitions.vendor_boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "vendor_v3"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
|
||||
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
|
||||
patched = "9f6342940b7cfbeb27b0567f006bb35cbee910ef038ec535403c662d5252ca71"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
[device.sunfish]
|
||||
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq2a.230305.008.c1-174fd16b.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 129700 },
|
||||
{ start = 476996, end = 34035261 },
|
||||
{ start = 1624751947, end = 1624756371 },
|
||||
{ start = 1823132563, end = 1823137581 },
|
||||
]
|
||||
hash.original.full = "174fd16b47ef994ea8f3cb0f3fb456df2654b0aa1f9ea6fb8e54e5c6319f2601"
|
||||
hash.original.stripped = "943ce3ae2aac8a0ccd4a7e9d4e38a9c495c39639f2621c6853be4c7a3fa0fc26"
|
||||
hash.patched.full = "cdfffa731f0aca0ab9eff1e7d7bfee8c4716054d64cf0a47e50f50da6cbdb849"
|
||||
hash.patched.stripped = "35b720058c460dd28b00f40bed4bcd1d133522d42d00ba048172b3092a7444ee"
|
||||
hash.avb_images."boot.img" = "22182f2efc7043f35d79e71abb400ce919c6b3b8419405e16469644932367ee6"
|
||||
hash.avb_images."vbmeta.img" = "4cbe171bb37515f59cd4082cc2d982ca1edfeba9db09dd62c8959dd318423888"
|
||||
hash.avb_images."vbmeta_system.img" = "7cdb590bfc1056a5a8c7606ff05e99eb344efe108296682698b5cfe83905e0cd"
|
||||
|
||||
# OnePlus 10 Pro
|
||||
# Build NE2215_11_C.26
|
||||
# What's unique:
|
||||
# - boot (boot v4) + recovery (boot v4)
|
||||
# - boot images have VTS signature block filled with all 0s
|
||||
# - payload.bin uses ZERO blocks
|
||||
# Build info:
|
||||
# - Unofficial list of full OTAs: https://forum.xda-developers.com/t/oneplus-10-pro-rom-ota-oxygen-os-repo-of-oxygen-os-builds.4572593/
|
||||
# - The North American builds are used because they're the only ones hosted on
|
||||
# a well known domain
|
||||
# - The build number can be found in <my_manifest>/build.prop since it's not
|
||||
# obvious from the filename
|
||||
[device.ossi]
|
||||
url = "https://android.googleapis.com/packages/ota-api/package/4cacbe5e6a3ab6a6fade68cc40f44d0fa6a2928a.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 204048 },
|
||||
{ start = 19105432, end = 34966775 },
|
||||
{ start = 2657405750, end = 2657407254 },
|
||||
{ start = 4984045446, end = 5006377281 },
|
||||
{ start = 5114504441, end = 5114507197 },
|
||||
{ start = 5138158449, end = 5138159817 },
|
||||
{ start = 5140101511, end = 5140105324 },
|
||||
]
|
||||
hash.original.full = "929f892fbd70699cf7f118a119aac1ae1b86351e1ada17715666fa4401e63472"
|
||||
hash.original.stripped = "4eabaf79b6c2b5df305e3ecdc2b9570c0dd27350b4e8d6434584000c4989ff3d"
|
||||
hash.patched.full = "8e9cf3159e57d706047325a7b774dc2dc84f0d56baae100ad54c0c723b621469"
|
||||
hash.patched.stripped = "0b5dcdfdfea742bf6662b286ba260f8572a7b0081bb1129b7274c29214cdfb49"
|
||||
hash.avb_images."boot.img" = "f5dc3b147c54589be8db00ca15257a3688424a9eb88bd9c2cec82ebf4f6bf859"
|
||||
hash.avb_images."recovery.img" = "a42c0bf4f023cd24394184a33ee113783a9c89a7cc4c0c582a5f72cc23b72309"
|
||||
hash.avb_images."vbmeta.img" = "c022cf79da301a8430af5c49704944c490707fa0306031fe3ea22c39ce4734f6"
|
||||
hash.avb_images."vbmeta_system.img" = "749616b7f04487c05e9e363ad2071a0ab3bae29d497daf1f1a7695f7c8cfa82a"
|
||||
hash.avb_images."vbmeta_vendor.img" = "a6037fce745384425fb12745b8568386b84fb57ca6f94f6e47bcf754de341ae4"
|
||||
[profile.pixel_v2]
|
||||
vabc_algo = "Gzip"
|
||||
|
||||
[profile.pixel_v2.partitions.boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
data.version = "v2"
|
||||
data.kernel = true
|
||||
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v2.partitions.system]
|
||||
avb.signed = false
|
||||
data.type = "dm_verity"
|
||||
data.content = "system_otacerts"
|
||||
|
||||
[profile.pixel_v2.partitions.vbmeta]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["boot", "vbmeta_system"]
|
||||
|
||||
[profile.pixel_v2.partitions.vbmeta_system]
|
||||
avb.signed = true
|
||||
data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
|
||||
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "52284308fae10cbaf09ade14e92f3bbe6149751a42bff15432982fcef8d890ab"
|
||||
patched = "7ad74ac87ddcaf34938017e6149a646041d70926e31ecda93e156e9397467b3b"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQjwBqCb7mn4vtIbEE
|
||||
v/daiQICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAMdJ698WAn+aEvKOQ70wGjBIIJ
|
||||
UIJbh4gk0vu1YP5FAPR6S1jvfSaf+hCmDq1mrK2nUt1sv5vDUFimKAJUYs0lLwuH
|
||||
1FCB3QkXrn+2Z2wcJoeA1nq0pp39nvGu4EBBTAHgTebwfaqvfNvRE9kAjGjMWpux
|
||||
BKaXxpSOC1V2JNfcxWi+reKwOweDzde9s5vpvPx7/637GPrLiMOvKWoUSg15jDbF
|
||||
u8T/hfY2TC0KiRLLT9b/8DZrscjXH1jAM7e3FK9F1PT9DSO9SfEkH3YRGPwkcnrl
|
||||
JcI6aPD0J3/YtXYrgcTdNNUdJy3Tbq328peXyw1ayumQTHfHahCNaIaKtH2WmKtx
|
||||
xUcaZnFD+AxPT52Dl41XD69tN6tWSENuf0ruaD6HsRRcALH4X5xqLKsObA/t2b6S
|
||||
m3bWcbKhuD5MGLcuw0oKbDuEYVJeZvuppsKhr2JsqqPBbnJc8FR59t2IGMYOteFV
|
||||
HD6oX0YxIZU+Cb3R3xF7gO7cn/NxBJetph9FBhn1yLEqZnU2uWpU0+WFY64J8S/1
|
||||
yDJsu7Sa9HShZpUQfLC/eRsQQQKaZrUsCHHC9MFFqy9zmsn1lIFUbqAJ6MTe3cf4
|
||||
QQJNhwNdBzSInBuI8sv5lwk6eidQyyRkaB+M8TrjhIwFRlCBd7XUbPACID+m/1jD
|
||||
DvzB/iV8GHhDW0gf8jOObfGDA9zsDt9R1fVgb4ehimNPhDV8ILIELqREo9DWTr4Q
|
||||
RGmjSwiyENi1Pp4PXS12NI0KbdJ5zuj5scwwLogs3Kdt3WMmqCZU6ektDeKuRI0W
|
||||
ZnnCEiHo7jinMo869ZhyI3BQykuSfRH24JuPX+F3vxHmPoFYDjHG8ZCVWyZEMHPl
|
||||
wRxok5k8jwe+sMOmRtQZPkcJFSiSYWyUciRED2oV1U5ojDo2LBvmQLjgPwCj/ECo
|
||||
v9DwRzsrljwI7tpIqBmHbWxcSxdDhdQNiRk0PU0bunNh1ubJkoyj/FdEPHOvexpR
|
||||
hCGzwDRqGRz5Mk73FI6ybCbBguo7m3ic6duMNhSYQdKTBPAilPjSpEB+a4awyjFv
|
||||
sn5zVciwgti/CtZJiFDNeGAxr4V18OO8qdDVL8Za/SvqZrr0Y0MJgnWM4RuB6lBl
|
||||
d+Rwub9ReJIhiAjTPIiJfHibxtDLEtXEfbSdE/a/Yn0xQYUYHKdbYJS0heumSF7D
|
||||
ihvGedIpW16YjXCb052xCxeN05rDzbm1m7UJlFivSfo/GXgQSH4W9xzG8yLCid8L
|
||||
PnptMXJFbI53mCJfjKy7ZV7rlx6dXf/uN620qwr/QCxBQfjM4ndA7X3UUAF/riHW
|
||||
MgaY5+GcBhRhGug1IcIZG/p8QB0t8Lt6Xssu/87uVSsc9pKEymVZwdhy3gU/YZY0
|
||||
dx6AsYk9E2n05g5AtafsDnik1gVgBb5KtMzW7XKYAA8805Ms4+gqeS9OUrevF6QS
|
||||
mRFHJpvaNuEYR6Lp6jLfwPH6MK+tZbE42St8XGLbFFgp3ncSe6UBhoGCYN7/hJkw
|
||||
QXPnK7vlgiO3NEV4d0IdZWRPUHXASYiEkpv2IYmUq8CYyqZuBERVs6eZmLIC7D9V
|
||||
y6kORASxDl0wEl4Nc0B0iZ0jhMGiNmG9EO2ek+YwlpB+vBm1C/Z+30qpzAnbnh22
|
||||
HWHn5g9koMa4oEkO7P5Bpp4UifxX/S5f0lAuLotaE6eYmwr9Tw7N9Za/mSLk2CuK
|
||||
akc/T/ycgQdGb/wWHvCzvm3EsbuAAxFDqhwAoobhkdvSyQek8HYJ2FiiXHgFY9kI
|
||||
RJ49AHCaiXwHInZwVzJd30NJFn1I/o9dLULwYPNsjfeULr5ZUSe95CYLL4LUxDFr
|
||||
ecd0YiuoA1Y8gIwIRAaFAs1wdkNZoiEj3vH3LMFM6DMlvwDCB+vOpFwmLRQOT5Vq
|
||||
DWPZTKNYkmU9ReWY6AM2VhmMZa+GHW7wbPKllPUCA/OmZQM98f1ivPXhADP+IAWt
|
||||
1zNy+hqhrB/HV820t7m8SEK9GRAj0ARfpV6b9LsUH91TBY4MXKSZW9BDlu51Nwcl
|
||||
zRZDIW1ZDA4cCI8JWnCALTNntuzcRx+y9pBRwxSOJXv6i08yOxDE7En1iiF79S6T
|
||||
91jo20SLGeazQm1iun2eOh/49xplktxED5m/4/aAIjwDxXpbGT9VzOZ86P0/z32j
|
||||
G0PngN173UGkmLDUZlaUavVrR3l85Uw6OxIgNMDaG8gQkbZT+iVLQs4MGiOJqFfc
|
||||
UqusOXh2E3WUkX+5cWIdv3GGwBvKw6NE09u1+RJMvqHB3NlOruSazDyTVgPC/IkQ
|
||||
iMSm1rGWdXQozaAur3jXQDYrmM4iV2ydDDcUc4FAgCwnT68073BemHvv9gPzTyex
|
||||
Wr8N98nmTepypAcgARDI2wlwaGm3fO5kvaxOq4fiqYK7PoTUVsS6OCfafhR7MNVQ
|
||||
lVpNHaDFS99ygjzjIvd2tQYY/Mwsvhp6YyMcXhtqTk0snlCGNFQh1rnDfEUFCGV7
|
||||
EaraeuvTk5+ofTd9S8Qm+weYddRkHeZVpEFAOa/cL/cevek7yv60pXDUPrmlWD29
|
||||
Ydu0VEO7DmVQnLm2gDprIQsZc01QxKRpCuRgwRgcpmAHX+xCFUWTCFL59mHW3F5x
|
||||
7VCl79NXYaO6Rp7GV6xTgjD5FMe0pP51s8+dUwTCq3U5Y5KqScvvHiPaatb0DuSI
|
||||
FcMzXRcapGrj5C42U2R7cpLq0xJi5QFapDD+wlEUs0lxZDiqf30Qr197a3KOOsWs
|
||||
pyoR/Ytry0nyHxy4CFvA5hbWc7ng2ZegykCkBVS6XM2upUbWxbPihb3v5UgDb3zD
|
||||
wXXwhjs+hwIh6xha7HlfH43HF8ucjLsPadw5EuQfQJ21a4dmaMbSDu1u6aPoKHsQ
|
||||
zF/DnySx5ZEdjNz9xMvz7g8LoRAWR4dMsuZFOBismy7KmMqFNsw0GNSRcDT5ffZf
|
||||
MmXbI654Jv4s6UMl2ghyK7xmYiEeSeVH41AF/LaZMl4V5KSNKuOhn764DzB6z0d4
|
||||
1XQrq3wCpJ3K4hknKZhTbgSAmvk7i8CwpWZfUWAzuBL9jS1Z6RoN/zgsjM5vDMSs
|
||||
QUEguBjytPKioiuhZh+F+buvoNDKWvYwlJk8tUyNwtFOhWjniend7zEeioVO/kdg
|
||||
abSrBmjqZkxbCVreNdDekiGwOz1iD1dlreW6lJ6hW0mBnSAafR/lwXLJCE1slTWf
|
||||
LIbM8DLZMMeKZ9v7M+VMlW0uL36utP2hMnzHnh9f9eYG
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
CWokJ23olHitbyjqw6aZoIFhAo3mwzwdEEkv7sjh600hPwR0YNUDxSkNA7ztK5Ii
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE6jCCAtKgAwIBAgIIG/hHrySZ878wDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE
|
||||
AwwHYXZicm9vdDAgFw0yMzEyMjYwMzQwNDBaGA8yMDUxMDUxMzAzNDA0MFowEjEQ
|
||||
MA4GA1UEAwwHYXZicm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
||||
AK9VVEeI2UnjQr3IrVlZddq2lTHOlV8qFBVzMguVJUr128HqIXQRgXbEzHfCOOzC
|
||||
Nuh2x+cBXO7gztpwjSJFi2P+q4OdEujmgCf8SdwxS1NqWo5TRsB9szPALeQ+FQi8
|
||||
yir+urv6uJUuDpwwPgjhCDwn1tL426c4HgGsQpO0IWjCzc2SWL+KP4zirk7GIf2R
|
||||
Fy0TsFeJi5ucHWiMuXCGyiPZ2xV7AZ3AakZnf6rqWEIkOOsBf6UVNxFKuppMBcY6
|
||||
Lb6YcXB7nn2xFwZ+S5gGdZjkZIlLAoZjRynVRBz2BwlJB6ignH8+MbWkYS6ls9C8
|
||||
C10SmiGXI/S/DhRR11JCNAC5AD8FO+A6i9Vp19PiVmUv6nTx01+FhRqTeiW5bPhk
|
||||
PTn9gUAU5FKL4ouhr3ojJd6BczdJ2lWB9lQYmWnwzdYnSAnvXwc3WaSX+ryi7LoJ
|
||||
sZtlc91ZT2yNiqmPQpH6fcPKfi/BJZhe61hZ7Xv3+OVupbmU+IuUK9iKEw5+aqNt
|
||||
DiT15PKoyKuAfm0A/TJ1UV/P4FbSavEOrMse2SZvhRL/EvwpRXwaiwPFQL/vZ5GZ
|
||||
mGCXdmVt0FEH9z3UyYq9+UIhZoQh3V9eSrOgjOh3dnFZN5E/UQzqndykGy+pYjG4
|
||||
e4ZZQjQR5G3pmAiCIngqnqRUNWpG5DQcZYuXLdor19o5AgMBAAGjQjBAMB0GA1Ud
|
||||
DgQWBBQgy5IQsP4+QnQRWlMJiFsqwyXAzTAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
|
||||
DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAn8pexl/crAWBPJ/nPu9rhRo0
|
||||
dowLhbamhx/i5RRNxIJl8Lpf78Boj4t9ICuTU4yeLFHfA+IcnthS/wrFADbhSgF5
|
||||
m4gLBSfRjtiOPJSJqkIqCIV6RIcbfdCJa/V3p1nvu3vuGYEp9eSYACEtv6kLaMFu
|
||||
61RsEWgR6tNmb1Hssl9XzGdM2yf2vy/Gip+Ugztz9gfTF3Vhdos9VToPOzLNQh6C
|
||||
dr+uLKnnPHOb9PfAonYs23MAfpBaP8m4sEMkyMgACCUP+qld0S8xpptEA/9Wblxy
|
||||
zWJIZTNWhyfo7cH030lVbdgMBYtZN/WxYVtD3fp+BZPk5fD3QwM712Ja288anDic
|
||||
HRwtc0wFwJ9EOOwXT1kYT1GgKrmLTnmAGDktnjKtsHMvl7Rn2Wf+5XTQ3FC99DJU
|
||||
q/Jhe3BGUvOXS7uPfUSMQrH4q+cwUdPSLGu2TSPyXVUEv5/Z4mH4JD+HQbJrKjdO
|
||||
NvUlnNJgtL3nIW8iSR1BRkIQn9HAnHtfYELHFBr1XHdn8LCjeWmYStdI3s9jucWb
|
||||
5uIfij1/50hc6A5zl20/y1h9OGFKdRttjk9XzRR6HPt22zyStjab5Y5SzHOwMCaP
|
||||
wk/uDc+Aestyqm7BoIqU8XptC+VV92OoNiEouI0UI5zPw9EWLRmTjFAc4NzOc1I7
|
||||
2AKjtz3xnKC3R3oqtsA=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,54 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQ1ucIEwTFlt1U7QDz
|
||||
w8o/RAICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAqttk+qw4JJKTbU1zgKChgBIIJ
|
||||
ULyEdMcpMWYvgN6aefcIns6UJr3bYfq8aX0FvhGXzO1N8Voxg/jKc753Hj3F+h1d
|
||||
xfJyFzpg3D2jIIR3GRZrXILqc+aXaW3AS8/U7IN2d4gJIrYAbUelBnssdgLg6FSY
|
||||
MUprWGUC8ShMZpOhAoDD2V8ZHyb9NHJ/xPpbhhBUvDl/jQWurCo3KnWlFiXiGO3I
|
||||
VyH09LL0T+v4OcU7HjrVHWXokcv+6mkcPvkZZI6MTMC+7UyxNi9WlZs98Lzckpax
|
||||
asiEHImnDbtGsvYXvjKWn7QXs1CF4SVZ5DdoHMtf1BEHMnXTQ4teSRkp8B02ihr6
|
||||
LtSoStVXdw1E3+5R6DmW8b+Bxi7duO9URazkH2dHG7ZJvhL86DcvsFNnMW7NZ3PE
|
||||
N0BYDGTPjSAp8qbkCesbzhYEoDFsTfp6S1vLB+9PL0/S8jJ76MgZJ5f6i69aEz62
|
||||
Ek7pbydMnrMlMoWmgNEzcywGf1GnNcSQfPUVUyW11HLShNUoD+PktrnPRHhti2cr
|
||||
VXfWNeaf9lEwnQQYPGEuwxx4+lfK/sL6TV245ziVFAWQ0koJHIF09nIlFKGT9sOa
|
||||
JrwXWSMg1iT2iua4OLvwsHs7oMMvC+C4ycEMS0NKPEVx3pyAPrsO+rAHnEeUIz/D
|
||||
ooZEjnSfE9Xlsb5W4qqPuso4H9s/ulFsGz312ZEnt3EDGrgW2BksG42F7G3Qm8nn
|
||||
wUzSEGAAs/iMO/qbVF6P5/LnoO7h+ds9GCudmBYPKj1o5cwLcyNPjBHbKAXlzvOJ
|
||||
xyx17aCJ2f7uZzto1ov05lr6N+0/ajRwQn9IIx3R1wFvssETav+fXLavvm0Ul065
|
||||
iQgpyQnmZeKgzXClwmc9HgoFIMQSzs3lfRgV2coQCvFMgKvP6cVLzMf98NUf7XmM
|
||||
/fL6aG62KGUVSaat+4mfPT8p8MiuGBz2BmTkEtTWMf+HsgPu3yYKHJ6iAF4JjZ2m
|
||||
xpDOAd2jACpVVQ+7fxyejSl/VILFuJPL7RKAvIjJzq40X3o/w2Ydih1LgGLiyFHe
|
||||
I9RXAqCkrA7rbN/TEtZkgpgsLUbbY2b5+rd++DoCIzB4zF6JSE1RTRYkR4jsxyFB
|
||||
BFwNqgZX/xpA0vQ4WqpYkAc4tsefJEXxt2SsI4C3pVYLYuso+4hY0ZP+n3QXseyV
|
||||
N6VKpbztWleHXeLI53Bmb5kj4oJk44ouO36A/DtUlEESfvtGrnqRzVG8vMIqhOOU
|
||||
sX8L+XmvCSnCkV8zDqaI/Wmr/X+4BQMjWJ3C0v3RBXJNCPWaQG9laNoHrcdCxGuR
|
||||
GCNArOhyxHfjl7jplw2YgNscPPd/GFuUOY3UEG7hWW14/cpGBz5D/PfKnq1QkKw+
|
||||
e5xvq/+BYGch3K0kOh3aEhbgPZxztIF+JZMcSokNDMPATE7zPpCxAYFCkasA/ccJ
|
||||
CTeyZqvLGMFYuMwe/rnPbgcxr/1fEEzgAsYkC6JqsIFq2uhEaDPrvmQ9LwAAwBfw
|
||||
EWhHpSOtMxEwOvzOqGuKDqUy918f5xSXyvKGPA8RC2ZPEEVHChQfyRRcS/9TRqhR
|
||||
6E0h6frMunWUdBtiDmm3iC6hllWVmwmXJL68aOWokCOf0hBKLl0tGw2QKsaeD3ta
|
||||
SV8478YUoPn2oNZSQW4QwncQMU/6djjtsL31MF6itZU3ethXbxOQWO3RkybFY1iL
|
||||
2eZmExFd9IdagxrI8xOxxMq7hYbAF+gB1vgcuzktz3/3L/lBAAl6n+9eAAK3E87U
|
||||
jhNDC/flJWxpdbNmwGoAUmGMt+oemdtE2gqDpCMgU/TEpuJcPrNYf3cce/CFIYOG
|
||||
tsYYPzp1QE63418/A7+9nJYTC2Bqqc0nLnARVXKMUpAUuf5xnRNvMod50xMZviJi
|
||||
aMdGcKnMVAk0esJG68/W3cQdK1efIeIhd7pFYUq1WdzBwfJV3BUYbXLV40qld50U
|
||||
EuxUPxWVCKD0soPvRl6DqLY7fE4gzEykwzjc2Y6l8oLuQfAzaNX/ItEVX+qvUXKe
|
||||
myZpKYqAqjOOdAZHLfyyFQzIMHzls83XhTMixlY53ZNMn3rs34LYR0F2k7is6Ysh
|
||||
qxulqwQ/EcY446WQyTw3XT0IPLIIBXQGpYsuYChn/U2QVSoJxSS1LIeOpMSNJOwk
|
||||
QnsPtxB6gK3Svt/wdnK0GCZEKwMBx9VAz+M1NKCQQMsv2xWdQ4qm6fOhMEdm/oq5
|
||||
Pc9pQrq2J9XwBI/3AncKvjxKCfRC6Ob6gEO4cUybbEAZ9r3GCGtdf2+9x/aJm5XP
|
||||
OeXXRYbwOQtaQp5GgAHE/WoGPWt+lXKLu5mUqrbONS6TLibdAYve9l4cFp3hTvki
|
||||
159bCm4HrCUj3Etu3x6Nv5SMhXhRdyULrOvx9GA7kco2arXcxSPM79KiyCPMVvyu
|
||||
e/sRn3X/dS1NfaJ5IlKYLjiJQVySJOmyNhmlMQkLErqjQ1M2ibwybFglrUdJflcd
|
||||
Bjk3ZbN0r/BqbWWrwVIUVkq6vr0Yuxb1AgEl6HH1QNcX67XLSrKFG5sGkfsImMTd
|
||||
gvEYKFukgXmqKX3/p9czjJMYlC6sFT0L3e5G3LiEJlGT5jxklEitcAvwTowhbhCy
|
||||
T0Jf/KTNn2GnisDUK2DZKRbhiAHv3AEkxAIGg6On5SJ0PzFF0MhtxJ7nA3TqzvUN
|
||||
gN7jAAftaiALPImQf1EDgj3rHu8O8XAC/Nac65Qvn2ZUjMOW+Cba1POal0G76XZx
|
||||
JunWIDu0BgOXNc7kBRmE+dK9ppEbD5DScUjosAvkZuF94h23Ofzwm9/xBgXPbaV4
|
||||
CbWBtvRLuKacuLKF3WfQCw/Wi0K70dwDUEAaM1/rJiavGzkYp1z0KPey3hKDzkFL
|
||||
2ZjkzQdCreXo48Jwfwu4OQFTpkACWLvB0pLbhBXjq5Q9NEJHizRr08YT0gl+5mbJ
|
||||
Dk+Aw/n2Wy5ZRVbxxklO5brLnu1R8Rq3Szq1ATENrXSHwIN3sUiOEuNu8tSa3If3
|
||||
Hb5WS1WELgVPzGaUWKsWI9sooTJEnPOXxBq0U0iZpqpnoUw6NenIgIfBWrvG/7LC
|
||||
5SPksOVuaD6YdC4OF35/Jlrk4uKnCsW5SYa69Kr5RST/8MFGrefb7233iiRAo3La
|
||||
vr+uvh4NGA9Jf7Pv4yuN2IuCsyAl2jleSDEj6cf/tTz6m9xPT3Nr1/pKrL8mkRxv
|
||||
HDHq2ivR6QjBpEEoHITMaYndgOvwpCA1qXdsP/DiugCx
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
R9iItBaS278KvqlLyil1yUlizNGvWI93SERQ6uAitbzXm4fOc9t3c8dG8r2ThuZz
|
||||
+51
-131
@@ -1,46 +1,23 @@
|
||||
/*
|
||||
* 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, path::PathBuf, str::FromStr};
|
||||
use std::{ffi::OsString, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use avbroot::cli::args::LogFormat;
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use tracing::Level;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct DeviceGroup {
|
||||
/// Device config name.
|
||||
pub struct ProfileGroup {
|
||||
/// OTA profile name.
|
||||
#[arg(short, long, value_name = "NAME")]
|
||||
pub device: Vec<String>,
|
||||
pub profile: Vec<String>,
|
||||
|
||||
/// All device configs.
|
||||
#[arg(short, long, conflicts_with = "device")]
|
||||
/// All profiles.
|
||||
#[arg(short, long, conflicts_with = "profile")]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct DownloadGroup {
|
||||
/// Revalidate hash of existing download.
|
||||
#[arg(long)]
|
||||
pub revalidate: bool,
|
||||
|
||||
/// Download the stripped OTA instead of the full OTA.
|
||||
#[arg(long)]
|
||||
pub stripped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct PatchGroup {
|
||||
/// Delete patched output files on success.
|
||||
#[arg(long)]
|
||||
pub delete_on_success: bool,
|
||||
|
||||
/// Suffix for patched output files.
|
||||
#[arg(long = "output-file-suffix", value_parser, default_value = ".patched")]
|
||||
pub suffix: OsString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ConfigGroup {
|
||||
/// Path to config file.
|
||||
@@ -53,112 +30,26 @@ pub struct ConfigGroup {
|
||||
)]
|
||||
pub config: PathBuf,
|
||||
|
||||
/// Working directory for storing images.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "DIRECTORY",
|
||||
value_parser,
|
||||
default_value = "files"
|
||||
)]
|
||||
pub work_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Convert a full OTA to stripped form.
|
||||
///
|
||||
/// A stripped OTA omits byte regions of the OTA that aren't needed for testing
|
||||
/// avbroot's patching logic (eg. the system partition image). This reduces the
|
||||
/// size of the test files by about two orders of magnitude.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct StripCli {
|
||||
/// Path to original OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
pub input: PathBuf,
|
||||
|
||||
/// Path to new stripped OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
pub output: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sha256Arg(pub [u8; 32]);
|
||||
|
||||
impl FromStr for Sha256Arg {
|
||||
type Err = hex::FromHexError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut data = [0u8; 32];
|
||||
hex::decode_to_slice(s, &mut data)?;
|
||||
Ok(Self(data))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new OTA image to the test config.
|
||||
///
|
||||
/// This will download the OTA image, strip it, patch both images, and add the
|
||||
/// resulting metadata (eg. checksums) to the specified test config file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddCli {
|
||||
/// URL to the full OTA zip.
|
||||
#[arg(short, long)]
|
||||
pub url: String,
|
||||
|
||||
/// Device config name.
|
||||
#[arg(short, long, value_name = "NAME")]
|
||||
pub device: String,
|
||||
|
||||
/// Expected sha256 hash of the full OTA zip.
|
||||
#[arg(short = 'H', long, value_name = "SHA256_HEX", value_parser)]
|
||||
pub hash: Option<Sha256Arg>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub patch: PatchGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
|
||||
/// Skip verifying OTA and AVB signatures.
|
||||
/// Working directory.
|
||||
///
|
||||
/// OTAs for some devices (eg. ossi) ship with vbmeta partitions containing
|
||||
/// invalid hashes. These will normally fail during validation.
|
||||
#[arg(long)]
|
||||
pub skip_verify: bool,
|
||||
}
|
||||
|
||||
/// Download a device image.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DownloadCli {
|
||||
/// Download the Magisk APK.
|
||||
#[arg(short, long)]
|
||||
pub magisk: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub device: DeviceGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub download: DownloadGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
/// If unset, a temporary directory is used, which will be automatically
|
||||
/// cleaned up, even if a failure occurs. Custom working directories are
|
||||
/// not deleted.
|
||||
#[arg(short, long, value_name = "DIRECTORY", value_parser)]
|
||||
pub work_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Run tests.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct TestCli {
|
||||
#[command(flatten)]
|
||||
pub device: DeviceGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub download: DownloadGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub patch: PatchGroup,
|
||||
pub profile: ProfileGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
}
|
||||
|
||||
/// List devices in config file.
|
||||
/// List profiles in config file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ListCli {
|
||||
#[command(flatten)]
|
||||
@@ -167,9 +58,6 @@ pub struct ListCli {
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
Strip(StripCli),
|
||||
Add(AddCli),
|
||||
Download(DownloadCli),
|
||||
Test(TestCli),
|
||||
List(ListCli),
|
||||
}
|
||||
@@ -178,4 +66,36 @@ pub enum Command {
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t = 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,
|
||||
}
|
||||
|
||||
+99
-106
@@ -1,17 +1,12 @@
|
||||
/*
|
||||
* 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::BTreeMap, fs, ops::Range, path::Path};
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use avbroot::format::payload::VabcAlgo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::{
|
||||
ser::ValueSerializer,
|
||||
visit_mut::{self, VisitMut},
|
||||
Array, Document, InlineTable, Item, KeyMut, Table, Value,
|
||||
};
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Sha256Hash(
|
||||
@@ -23,118 +18,116 @@ pub struct Sha256Hash(
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Magisk {
|
||||
pub url: String,
|
||||
pub hash: Sha256Hash,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OtaInfo {
|
||||
pub device: String,
|
||||
pub fingerprint: String,
|
||||
pub build_number: String,
|
||||
pub incremental_version: String,
|
||||
pub android_version: String,
|
||||
pub sdk_version: String,
|
||||
pub security_patch_level: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct OtaHashes {
|
||||
pub full: Sha256Hash,
|
||||
pub stripped: Sha256Hash,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Avb {
|
||||
pub signed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RamdiskContent {
|
||||
Init,
|
||||
Otacerts,
|
||||
FirstStage,
|
||||
DsuKeyDir,
|
||||
Dlkm,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BootVersion {
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
VendorV3,
|
||||
VendorV4,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ImageHashes {
|
||||
pub original: OtaHashes,
|
||||
pub patched: OtaHashes,
|
||||
pub avb_images: BTreeMap<String, Sha256Hash>,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BootData {
|
||||
pub version: BootVersion,
|
||||
#[serde(default)]
|
||||
pub kernel: bool,
|
||||
#[serde(default)]
|
||||
pub ramdisks: Vec<Vec<RamdiskContent>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DmVerityContent {
|
||||
SystemOtacerts,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Device {
|
||||
pub url: String,
|
||||
pub sections: Vec<Range<u64>>,
|
||||
pub hash: ImageHashes,
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DmVerityData {
|
||||
pub content: DmVerityContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VbmetaData {
|
||||
pub deps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Data {
|
||||
Boot(BootData),
|
||||
DmVerity(DmVerityData),
|
||||
Vbmeta(VbmetaData),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Hashes {
|
||||
pub original: Sha256Hash,
|
||||
pub patched: Sha256Hash,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Partition {
|
||||
pub avb: Avb,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Profile {
|
||||
pub vabc_algo: Option<VabcAlgo>,
|
||||
pub partitions: BTreeMap<String, Partition>,
|
||||
pub hashes_streaming: Hashes,
|
||||
pub hashes_seekable: Hashes,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub magisk: Magisk,
|
||||
pub device: BTreeMap<String, Device>,
|
||||
pub ota_info: OtaInfo,
|
||||
#[serde(default)]
|
||||
pub profile: BTreeMap<String, Profile>,
|
||||
}
|
||||
|
||||
struct ConfigFormatter;
|
||||
|
||||
impl VisitMut for ConfigFormatter {
|
||||
fn visit_table_like_kv_mut(&mut self, key: KeyMut<'_>, node: &mut Item) {
|
||||
// Convert non-array-of-tables inline tables into regular tables.
|
||||
if let Item::Value(Value::InlineTable(t)) = node {
|
||||
let inline_table = std::mem::replace(t, InlineTable::new());
|
||||
*node = Item::Table(inline_table.into_table());
|
||||
}
|
||||
|
||||
// But for hashes, use dotted notation until TOML 1.1, which allows
|
||||
// newlines in inline tables, is released.
|
||||
if key == "hash" || key == "original" || key == "patched" || key == "avb_images" {
|
||||
if let Some(t) = node.as_table_like_mut() {
|
||||
t.set_dotted(true);
|
||||
}
|
||||
}
|
||||
|
||||
visit_mut::visit_table_like_kv_mut(self, key, node);
|
||||
}
|
||||
|
||||
fn visit_table_mut(&mut self, node: &mut Table) {
|
||||
// Make tables implicit unless they are empty, which may be meaningful.
|
||||
if !node.is_empty() {
|
||||
node.set_implicit(true);
|
||||
}
|
||||
|
||||
visit_mut::visit_table_mut(self, node);
|
||||
}
|
||||
|
||||
fn visit_array_mut(&mut self, node: &mut Array) {
|
||||
visit_mut::visit_array_mut(self, node);
|
||||
|
||||
// Put array elements on their own indented lines.
|
||||
if node.is_empty() {
|
||||
node.set_trailing("");
|
||||
node.set_trailing_comma(false);
|
||||
} else {
|
||||
for item in node.iter_mut() {
|
||||
item.decor_mut().set_prefix("\n ");
|
||||
}
|
||||
node.set_trailing("\n");
|
||||
node.set_trailing_comma(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a device to the config file. This leaves all comments intact, except for
|
||||
/// those contained within the existing device section if it exists.
|
||||
pub fn add_device(document: &mut Document, name: &str, device: &Device) -> Result<()> {
|
||||
let device_table = document.entry("device").or_insert_with(|| {
|
||||
let mut t = toml_edit::Table::new();
|
||||
t.set_implicit(true);
|
||||
Item::Table(t)
|
||||
});
|
||||
let old_table = device_table.get(name).and_then(|i| i.as_table());
|
||||
|
||||
let value = device.serialize(ValueSerializer::new())?;
|
||||
let Value::InlineTable(inline_table) = value else {
|
||||
unreachable!("Device did not serialize as an inline table");
|
||||
};
|
||||
let mut table = inline_table.into_table();
|
||||
|
||||
ConfigFormatter.visit_table_mut(&mut table);
|
||||
|
||||
// Keep top-level comment on the table.
|
||||
if let Some(t) = old_table {
|
||||
*table.decor_mut() = t.decor().clone();
|
||||
}
|
||||
|
||||
device_table[name] = Item::Table(table);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub 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(|| anyhow!("Failed to read config: {path:?}"))?;
|
||||
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
|
||||
let config: Config = toml_edit::de::from_str(&contents)
|
||||
.with_context(|| anyhow!("Failed to parse config: {path:?}"))?;
|
||||
let document: Document = contents.parse().unwrap();
|
||||
.with_context(|| format!("Failed to parse config: {path:?}"))?;
|
||||
let document: DocumentMut = contents.parse().unwrap();
|
||||
|
||||
Ok((config, document))
|
||||
}
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use avbroot::stream::PSeekFile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
runtime::Runtime,
|
||||
signal::ctrl_c,
|
||||
sync::{mpsc, oneshot},
|
||||
task::{self, JoinSet},
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
/// Minimum download chunk size per task.
|
||||
const MIN_CHUNK_SIZE: u64 = 1024 * 1024;
|
||||
|
||||
pub trait ProgressDisplay {
|
||||
fn progress(&mut self, current: u64, total: u64);
|
||||
|
||||
fn error(&mut self, msg: &str);
|
||||
|
||||
fn finish(&mut self);
|
||||
}
|
||||
|
||||
pub struct BasicProgressDisplay {
|
||||
current: u64,
|
||||
total: u64,
|
||||
interval: Duration,
|
||||
last_render: Instant,
|
||||
avg: VecDeque<(Instant, u64)>,
|
||||
}
|
||||
|
||||
// Speed is a simple moving average over 5 seconds.
|
||||
static AVG_INTERVAL: Duration = Duration::from_millis(100);
|
||||
static AVG_WINDOW_SIZE: usize = 5000 / AVG_INTERVAL.as_millis() as usize;
|
||||
|
||||
impl BasicProgressDisplay {
|
||||
pub fn new(interval: Duration) -> Self {
|
||||
Self {
|
||||
current: 0,
|
||||
total: 0,
|
||||
interval,
|
||||
last_render: Instant::now() - interval,
|
||||
avg: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_line(&self) {
|
||||
eprint!("\x1b[2K\r");
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressDisplay for BasicProgressDisplay {
|
||||
fn progress(&mut self, current: u64, total: u64) {
|
||||
self.current = current;
|
||||
self.total = total;
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
if self.avg.is_empty() || (now - self.avg.back().unwrap().0) > AVG_INTERVAL {
|
||||
if self.avg.len() == AVG_WINDOW_SIZE {
|
||||
self.avg.pop_front();
|
||||
}
|
||||
|
||||
self.avg.push_back((now, current));
|
||||
}
|
||||
|
||||
if now - self.last_render > self.interval {
|
||||
let current_mib = current as f64 / 1024.0 / 1024.0;
|
||||
let total_mib = total as f64 / 1024.0 / 1024.0;
|
||||
|
||||
let front = self.avg.front().unwrap();
|
||||
let back = self.avg.back().unwrap();
|
||||
|
||||
let avg_window_mib = (back.1 - front.1) as f64 / 1024.0 / 1024.0;
|
||||
let avg_window_duration = back.0 - front.0;
|
||||
|
||||
let speed_mib_s = if avg_window_duration.is_zero() {
|
||||
0.0
|
||||
} else {
|
||||
avg_window_mib / avg_window_duration.as_secs_f64()
|
||||
};
|
||||
|
||||
self.clear_line();
|
||||
eprint!("{current_mib:.1} / {total_mib:.1} MiB ({speed_mib_s:.1} MiB/s)");
|
||||
|
||||
self.last_render = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn error(&mut self, msg: &str) {
|
||||
self.clear_line();
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
self.clear_line();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProgressMessage {
|
||||
task_id: u64,
|
||||
bytes: u64,
|
||||
// Controller replies with new ending offset
|
||||
resp: oneshot::Sender<u64>,
|
||||
}
|
||||
|
||||
/// Download a contiguous byte range. The number of bytes downloaded per loop
|
||||
/// iteration will be sent to the specified channel via a `ProgressMessage`. The
|
||||
/// receiver of the message must reply with the new ending offset for this
|
||||
/// download via the oneshot channel in the `resp` field. An appropriate error
|
||||
/// will be returned if the full range (subject to modification) cannot be fully
|
||||
/// downloaded (eg. premature EOF is an error).
|
||||
async fn download_range(
|
||||
task_id: u64,
|
||||
url: &str,
|
||||
mut file: PSeekFile,
|
||||
initial_range: Range<u64>,
|
||||
channel: mpsc::Sender<ProgressMessage>,
|
||||
) -> Result<()> {
|
||||
assert!(initial_range.start < initial_range.end);
|
||||
|
||||
let client = reqwest::ClientBuilder::new().build()?;
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.header(
|
||||
reqwest::header::RANGE,
|
||||
format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.with_context(|| anyhow!("Failed to start download for range: {initial_range:?}"))?;
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut range = initial_range.clone();
|
||||
|
||||
while range.start < range.end {
|
||||
let data = if let Some(x) = stream.next().await {
|
||||
x?
|
||||
} else {
|
||||
return Err(anyhow!("Unexpected EOF from server"));
|
||||
};
|
||||
|
||||
// This may overlap with another task's write when a range split occurs,
|
||||
// but the same data will be written anyway, so it's not a huge deal.
|
||||
task::block_in_place(|| {
|
||||
file.seek(SeekFrom::Start(range.start))?;
|
||||
file.write_all(&data)
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to write {} bytes to output file at offset {}",
|
||||
data.len(),
|
||||
range.start,
|
||||
)
|
||||
})?;
|
||||
|
||||
let consumed = cmp::min(range.end - range.start, data.len() as u64);
|
||||
range.start += consumed;
|
||||
|
||||
// Report progress to the controller.
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let msg = ProgressMessage {
|
||||
task_id,
|
||||
bytes: consumed,
|
||||
resp: tx,
|
||||
};
|
||||
channel.send(msg).await?;
|
||||
|
||||
// Get new ending offset from controller.
|
||||
let new_end = rx.await?;
|
||||
if new_end != range.end {
|
||||
debug_assert!(new_end <= range.end);
|
||||
range.end = new_end;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create download task for a byte range. This just calls [`download_range()`]
|
||||
/// and returns a tuple containing the task ID and the result.
|
||||
async fn download_task(
|
||||
task_id: u64,
|
||||
url: String,
|
||||
file: PSeekFile,
|
||||
initial_range: Range<u64>,
|
||||
channel: mpsc::Sender<ProgressMessage>,
|
||||
) -> (u64, Result<()>) {
|
||||
(
|
||||
task_id,
|
||||
download_range(task_id, &url, file, initial_range, channel).await,
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a HEAD request to get the value of the Content-Length header.
|
||||
async fn get_content_length(url: &str) -> Result<u64> {
|
||||
let response = reqwest::Client::new()
|
||||
.head(url)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.context("Failed to send HEAD request to get Content-Length")?;
|
||||
|
||||
response
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.parse().ok())
|
||||
.ok_or_else(|| anyhow!("HEAD request did not return a valid Content-Length"))
|
||||
}
|
||||
|
||||
/// Download a set of file chunks in parallel. Only unrecoverable errors are
|
||||
/// returned as an Err. Normal/expected errors and download progress info are
|
||||
/// reported via `display`. Returns the remaining ranges that need to be
|
||||
/// downloaded.
|
||||
async fn download_ranges(
|
||||
url: &str,
|
||||
output: &Path,
|
||||
initial_ranges: Option<&[Range<u64>]>,
|
||||
display: &mut dyn ProgressDisplay,
|
||||
max_tasks: usize,
|
||||
max_errors: u8,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
let file_size = get_content_length(url).await?;
|
||||
|
||||
// Open for writing, but without truncation.
|
||||
let file = task::block_in_place(|| {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for writing: {output:?}"))
|
||||
})?;
|
||||
|
||||
task::block_in_place(|| file.set_len(file_size))
|
||||
.with_context(|| anyhow!("Failed to set file size: {output:?}"))?;
|
||||
|
||||
// Queue of ranges that need to be downloaded.
|
||||
let mut remaining = VecDeque::from(match initial_ranges {
|
||||
Some(r) => r.to_vec(),
|
||||
#[allow(clippy::single_range_in_vec_init)]
|
||||
None => vec![0..file_size],
|
||||
});
|
||||
// Ranges that have failed.
|
||||
let mut failed = Vec::<Range<u64>>::new();
|
||||
// Ranges for currently running tasks.
|
||||
let mut task_ranges = HashMap::<u64, Range<u64>>::new();
|
||||
|
||||
// Overall progress.
|
||||
let mut progress = file_size - remaining.iter().map(|r| r.end - r.start).sum::<u64>();
|
||||
display.progress(progress, file_size);
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
let mut next_task_id = 0;
|
||||
let mut error_count = 0u8;
|
||||
// Progress messages from tasks.
|
||||
let (tx, mut rx) = mpsc::channel(max_tasks);
|
||||
|
||||
loop {
|
||||
// Spawn new tasks.
|
||||
while tasks.len() < max_tasks {
|
||||
if remaining.is_empty() && !tasks.is_empty() {
|
||||
// No more ranges to download. Split another task's range.
|
||||
let (_, old_range) = task_ranges
|
||||
.iter_mut()
|
||||
.max_by_key(|(_, r)| r.end - r.start)
|
||||
.unwrap();
|
||||
let size = old_range.end - old_range.start;
|
||||
|
||||
if size >= MIN_CHUNK_SIZE {
|
||||
let new_range = old_range.start + size / 2..old_range.end;
|
||||
old_range.end = new_range.start;
|
||||
remaining.push_back(new_range);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task_range) = remaining.pop_front() {
|
||||
tasks.spawn(download_task(
|
||||
next_task_id,
|
||||
url.to_owned(),
|
||||
file.clone(),
|
||||
task_range.clone(),
|
||||
tx.clone(),
|
||||
));
|
||||
|
||||
task_ranges.insert(next_task_id, task_range);
|
||||
next_task_id += 1;
|
||||
} else {
|
||||
// No pending ranges and no running tasks can be split.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
// Interrupted by user.
|
||||
c = ctrl_c() => {
|
||||
c?;
|
||||
break;
|
||||
}
|
||||
|
||||
// Received progress notification.
|
||||
msg = rx.recv() => {
|
||||
let msg = msg.unwrap();
|
||||
|
||||
progress += msg.bytes;
|
||||
display.progress(progress, file_size);
|
||||
|
||||
let task_range = task_ranges.get_mut(&msg.task_id).unwrap();
|
||||
task_range.start += msg.bytes;
|
||||
|
||||
msg.resp.send(task_range.end).unwrap();
|
||||
}
|
||||
|
||||
// Received completion message.
|
||||
r = tasks.join_next() => {
|
||||
match r {
|
||||
// All tasks exited.
|
||||
None => {
|
||||
break;
|
||||
},
|
||||
|
||||
// Download task panicked.
|
||||
Some(Err(e)) => {
|
||||
return Err(e).context("Unexpected panic in download task");
|
||||
}
|
||||
|
||||
// Task completed successfully.
|
||||
Some(Ok((task_id, Ok(_)))) => {
|
||||
task_ranges.remove(&task_id).unwrap();
|
||||
}
|
||||
|
||||
// Task failed.
|
||||
Some(Ok((task_id, Err(e)))) => {
|
||||
display.error(&format!("[Task#{task_id}] {e}"));
|
||||
error_count += 1;
|
||||
|
||||
let range = task_ranges.remove(&task_id).unwrap();
|
||||
|
||||
if error_count < max_errors {
|
||||
remaining.push_back(range);
|
||||
} else {
|
||||
failed.push(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
display.finish();
|
||||
|
||||
failed.extend(remaining.into_iter());
|
||||
failed.extend(task_ranges.into_values());
|
||||
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct State {
|
||||
ranges: Vec<Range<u64>>,
|
||||
}
|
||||
|
||||
fn read_state(path: &Path) -> Result<Option<State>> {
|
||||
let data = match fs::read_to_string(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => Err(e).with_context(|| anyhow!("Failed to read download state: {path:?}"))?,
|
||||
};
|
||||
|
||||
let state = toml_edit::de::from_str(&data)
|
||||
.with_context(|| anyhow!("Failed to parse download state: {path:?}"))?;
|
||||
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
fn write_state(path: &Path, state: &State) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string(state).unwrap();
|
||||
|
||||
fs::write(path, data).with_context(|| anyhow!("Failed to write download state: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_if_exists(path: &Path) -> Result<()> {
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
if e.kind() != io::ErrorKind::NotFound {
|
||||
return Err(e).context(format!("Failed to delete file: {path:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn state_path(path: &Path) -> PathBuf {
|
||||
let mut s = path.as_os_str().to_owned();
|
||||
s.push(".state");
|
||||
PathBuf::from(s)
|
||||
}
|
||||
|
||||
/// Download `url` to `output` with parallel threads.
|
||||
///
|
||||
/// If `initial_ranges` is specified, only those sections of the file will be
|
||||
/// downloaded. The empty regions are left untouched (i.e. filled with zeroes).
|
||||
/// A `.state` file is written if the download is interrupted. If the state
|
||||
/// file exists when this function is called, `initial_ranges` is ignored and
|
||||
/// the ranges from the state file are used to resume the download.
|
||||
pub fn download(
|
||||
url: &str,
|
||||
output: &Path,
|
||||
initial_ranges: Option<&[Range<u64>]>,
|
||||
display: &mut dyn ProgressDisplay,
|
||||
max_tasks: usize,
|
||||
max_errors: u8,
|
||||
) -> Result<()> {
|
||||
let state_path = state_path(output);
|
||||
let ranges = match read_state(&state_path)? {
|
||||
Some(r) => Some(r.ranges),
|
||||
None => initial_ranges.map(|r| r.to_vec()),
|
||||
};
|
||||
|
||||
let runtime = Runtime::new()?;
|
||||
let remaining = runtime.block_on(download_ranges(
|
||||
url,
|
||||
output,
|
||||
ranges.as_deref(),
|
||||
display,
|
||||
max_tasks,
|
||||
max_errors,
|
||||
))?;
|
||||
|
||||
if remaining.is_empty() {
|
||||
delete_if_exists(&state_path)?;
|
||||
} else {
|
||||
write_state(&state_path, &State { ranges: remaining })?;
|
||||
bail!("Download was interrupted");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+1245
-619
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
/hfuzz_target/
|
||||
HONGGFUZZ.REPORT.TXT
|
||||
*.honggfuzz.cov
|
||||
*.fuzz
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "fuzz"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
# There's currently no good way to exclude a workspace member for a specific
|
||||
# platform, so we just compile empty stubs on Windows (which isn't supported by
|
||||
# honggfuzz).
|
||||
# https://github.com/rust-lang/cargo/issues/5220
|
||||
# https://github.com/rust-lang/cargo/issues/6179
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
avbroot = { path = "../avbroot" }
|
||||
honggfuzz = "0.5.55"
|
||||
@@ -0,0 +1,53 @@
|
||||
# Fuzzing
|
||||
|
||||
While avbroot's parsers are all memory-safe, it is still possible for panics or crashes to occur, for example due to excessive memory allocation, integer overflow, or division by zero. Fuzzing helps to identify these issues by randomizing inputs in a way that tries to increase code coverage.
|
||||
|
||||
## Running the fuzzers
|
||||
|
||||
1. Install the cargo honggfuzz commands.
|
||||
|
||||
```bash
|
||||
cargo install honggfuzz
|
||||
```
|
||||
|
||||
2. Pick a fuzz target to run. A fuzz target is the name of the source file in [`src/bin/`](./src/bin) without the `.rs` extension.
|
||||
|
||||
The list of targets can be queried programmatically with:
|
||||
|
||||
```bash
|
||||
cargo read-manifest | jq -r '.targets[].name'
|
||||
```
|
||||
|
||||
3. Run the fuzzer.
|
||||
|
||||
```bash
|
||||
cargo hfuzz run <fuzz target>
|
||||
```
|
||||
|
||||
This will run forever until it is manually killed. At the top of the screen, a summary section like the following is shown:
|
||||
|
||||
```
|
||||
Iterations : 31,243 [31.24k]
|
||||
Mode [1/3] : Feedback Driven Dry Run [2486/4085]
|
||||
Target : hfuzz_target/x86_64-unknown-linux-gnu/release/bootimage
|
||||
Threads : 8, CPUs: 16, CPU%: 800% [50%/CPU]
|
||||
Speed : 36,126/sec [avg: 31,243]
|
||||
Crashes : 53 [unique: 1, blocklist: 0, verified: 0]
|
||||
Timeouts : 0 [1 sec]
|
||||
Corpus Size : 1,424, max: 24,576 bytes, init: 4,085 files
|
||||
Cov Update : 0 days 00 hrs 00 mins 00 secs ago
|
||||
Coverage : edge: 897/224,621 [0%] pc: 2 cmp: 34,736
|
||||
```
|
||||
|
||||
When a crash occurs, the `Crashes` counter will increment and the input data that triggered the crash will be written to `hfuzz_workspace/<fuzz target>/*.fuzz`. New files are only written for unique crashes.
|
||||
|
||||
4. If a crash occurs, run the following command to trigger the crash in a debugger.
|
||||
|
||||
```bash
|
||||
cargo hfuzz run-debug <fuzz target> \
|
||||
hfuzz_workspace/<fuzz_target>/<input file>.fuzz
|
||||
```
|
||||
|
||||
This defaults to using `rust-lldb`. To use `rust-gdb` instead, set the `HFUZZ_DEBUGGER` environment variable to `rust-gdb`.
|
||||
|
||||
Alternatively, just feed the input file to the appropriate avbroot command directly (eg. `avbroot boot info -i hfuzz_workspace/<fuzz_target>/<input file>.fuzz` for boot images).
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vbmeta_appended_hash.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vbmeta_appended_hash_tree.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vbmeta_root.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v0.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v1.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v2.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v3.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v4.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v4_vts.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vendor_v3.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vendor_v4.img
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user