mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
305 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1179f9f2fa | |||
| bbe793bd2d | |||
| d643b51579 | |||
| a25d9a9eb4 | |||
| b92513609b | |||
| 44d62f5147 | |||
| fb4b93b403 | |||
| c6ac508a50 | |||
| 6d892d37ff | |||
| 8549fa1dfc |
@@ -0,0 +1,5 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
@@ -1,53 +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: tests/files/${{ inputs.device }}-sparse.tar
|
||||
|
||||
- if: ${{ steps.cache-img.outputs.cache-hit }}
|
||||
name: Extracting image from sparse archive
|
||||
shell: sh
|
||||
run: |
|
||||
tar -C tests/files -xf tests/files/${{ inputs.device }}-sparse.tar
|
||||
|
||||
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: python3-lz4 python3-protobuf
|
||||
|
||||
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: python3-strictyaml
|
||||
|
||||
- name: Downloading device image for ${{ inputs.device }}
|
||||
if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
shell: sh
|
||||
run: |
|
||||
./tests/tests.py \
|
||||
download \
|
||||
--stripped \
|
||||
--no-magisk \
|
||||
--device \
|
||||
${{ inputs.device }}
|
||||
|
||||
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
name: Creating sparse archive from image
|
||||
shell: sh
|
||||
run: |
|
||||
cd tests/files
|
||||
tar --sparse -cf ${{ inputs.device }}-sparse.tar \
|
||||
${{ inputs.device }}/*.stripped
|
||||
@@ -1,34 +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: tests/files/magisk
|
||||
|
||||
- if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: python3-lz4 python3-protobuf
|
||||
|
||||
- if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: python3-strictyaml
|
||||
|
||||
- name: Downloading Magisk
|
||||
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
shell: sh
|
||||
run: |
|
||||
./tests/tests.py \
|
||||
download \
|
||||
--magisk \
|
||||
--no-devices
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Preload tox cache
|
||||
inputs:
|
||||
cache-key-prefix:
|
||||
description: 'Tox cache-key prefix'
|
||||
required: true
|
||||
python-version:
|
||||
description: 'Python version'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ inputs.cache-key-prefix }}${{ inputs.python-version }}
|
||||
restore-keys: |
|
||||
tox-
|
||||
# Make sure any changes to path are also reflected in ci.yml setup
|
||||
path: |
|
||||
.tox/
|
||||
~/.cache/pip
|
||||
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: tox
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: |
|
||||
${{ fromJson('{ "py39": "3.9", "py310": "3.10", "py311": "3.11" }')[inputs.python-version] }}
|
||||
+112
-157
@@ -5,173 +5,128 @@ 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:
|
||||
setup:
|
||||
name: Prepare workflow data
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
tox-key-prefix: ${{ steps.cache-keys.outputs.tox-key-prefix }}
|
||||
tox-hit: ${{ steps.get-tox-cache.outputs.cache-matched-key }}
|
||||
build:
|
||||
runs-on: ${{ matrix.artifact.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# https://github.com/rust-lang/rust/issues/78210
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: python3-strictyaml
|
||||
|
||||
- name: Loading test config
|
||||
id: load-config
|
||||
shell: python
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
echo -n 'version=' >> "${GITHUB_OUTPUT}"
|
||||
git describe --always \
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
sys.path.append(os.environ['GITHUB_WORKSPACE'])
|
||||
import tests.config
|
||||
|
||||
config_data = tests.config.load_config()
|
||||
devices = [d.data for d in config_data['device']]
|
||||
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write(f'config-path={tests.config.CONFIG_PATH}\n')
|
||||
f.write(f"device-list={json.dumps(devices)}\n")
|
||||
|
||||
- name: Generating cache keys
|
||||
id: cache-keys
|
||||
- name: Install toolchains
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "tox-key-prefix=tox-${{ hashFiles('tox.ini') }}-"; \
|
||||
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
|
||||
for target in ${TARGETS}; do
|
||||
rustup target add "${target}"
|
||||
done
|
||||
|
||||
- name: Checking for cached tox environments
|
||||
id: get-tox-cache
|
||||
uses: actions/cache/restore@v3
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Clippy
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo clippy --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo build --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
- name: Tests
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo test --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
- name: End to end tests
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo 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/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
key: ${{ steps.cache-keys.outputs.tox-key-prefix }}
|
||||
lookup-only: true
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
.tox/
|
||||
~/.cache/pip
|
||||
|
||||
- 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: |
|
||||
tests/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: tests/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
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Preloading image cache
|
||||
uses: ./.github/actions/preload-img-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
|
||||
device: ${{ matrix.device }}
|
||||
|
||||
preload-tox:
|
||||
name: Preload tox environments
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
timeout-minutes: 5
|
||||
# Assume that preloading always succesfully cached all tox environments 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.tox-hit }}
|
||||
strategy:
|
||||
matrix:
|
||||
python: [py39, py310, py311]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Preloading tox cache
|
||||
uses: ./.github/actions/preload-tox-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.tox-key-prefix }}
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Generating tox environment
|
||||
run: tox -e ${{ matrix.python }} --notest
|
||||
|
||||
tests:
|
||||
name: Run test for ${{ matrix.device }} with ${{ matrix.python }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [setup, preload-img, preload-tox]
|
||||
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) }}
|
||||
python: [py39, py310, py311]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- 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: Restoring tox cache
|
||||
uses: ./.github/actions/preload-tox-cache
|
||||
with:
|
||||
cache-key-prefix: ${{ needs.setup.outputs.tox-key-prefix }}
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
# Finally run tests
|
||||
- name: Run test for ${{ matrix.device }} with ${{ matrix.python }}
|
||||
run: tox -e ${{ matrix.python }} -- --stripped -d ${{ matrix.device }}
|
||||
target/output/LICENSE
|
||||
target/output/README.md
|
||||
target/output/avbroot
|
||||
target/output/avbroot.exe
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: cargo-deny
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
check:
|
||||
name: cargo-deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
build-app:
|
||||
name: Build modules
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: echo "version=r$(git rev-list --count HEAD).$(git rev-parse --short HEAD)" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build and test
|
||||
run: ./modules/build.py
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: avbroot-modules-${{ steps.get_version.outputs.version }}
|
||||
path: modules/dist/
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Github Release
|
||||
on:
|
||||
push:
|
||||
# Uncomment to test against a branch
|
||||
#branches:
|
||||
# - ci
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
create_release:
|
||||
name: Create Github release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Get version from tag
|
||||
id: get_version
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||
version=${GITHUB_REF#refs/tags/v}
|
||||
else
|
||||
version=0.0.0.${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
body_path: RELEASE.md
|
||||
draft: true
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
# Caches
|
||||
__pycache__/
|
||||
# Build directories
|
||||
/target/
|
||||
|
||||
# Secrets
|
||||
*.pem
|
||||
@@ -11,4 +11,6 @@ __pycache__/
|
||||
*.img
|
||||
*.zip
|
||||
*.patched
|
||||
.tox
|
||||
|
||||
# We do want the test images
|
||||
!/avbroot/tests/data/*.img
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[submodule "external/avb"]
|
||||
path = external/avb
|
||||
url = https://android.googlesource.com/platform/external/avb/
|
||||
[submodule "external/update_engine"]
|
||||
path = external/update_engine
|
||||
url = https://android.googlesource.com/platform/system/update_engine
|
||||
[submodule "external/build"]
|
||||
path = external/build
|
||||
url = https://android.googlesource.com/platform/build
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
<!--
|
||||
When adding new changelog entries, use [Issue #0] to link to issues and
|
||||
[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.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])
|
||||
|
||||
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
|
||||
[Discussion #195]: https://github.com/chenxiaolong/avbroot/discussions/195
|
||||
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
|
||||
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
|
||||
[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
|
||||
[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 #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
|
||||
Generated
+2108
File diff suppressed because it is too large
Load Diff
+10
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
default-members = ["avbroot"]
|
||||
members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.2.1"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
# avbroot extra
|
||||
|
||||
avbroot includes several feature-complete parsers for various things, like boot images. Some of these are exposed as extra subcommands. They aren't needed for normal OTA patching, but may be useful in other scenarios.
|
||||
|
||||
Note that while avbroot maintains a stable command line interface for the patching-related subcommands, these extra subcommands do not have backwards compatibility guarantees.
|
||||
|
||||
## `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`.
|
||||
|
||||
### 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 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.
|
||||
|
||||
### Verifying AVB hashes and signatures
|
||||
|
||||
```bash
|
||||
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 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`.
|
||||
|
||||
## `avbroot boot`
|
||||
|
||||
### Unpacking a boot image
|
||||
|
||||
```bash
|
||||
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 `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
|
||||
|
||||
```bash
|
||||
avbroot boot pack -o <output boot image>
|
||||
```
|
||||
|
||||
This subcommand packs a new boot image from the individual components in the current directory by default (see `--help`). The default input filenames are the same as the output filenames for the `unpack` subcommand.
|
||||
|
||||
### Repacking a boot image
|
||||
|
||||
```bash
|
||||
avbroot boot repack -i <input boot image> -o <output boot image>
|
||||
```
|
||||
|
||||
This subcommand repacks a boot image without writing the individual components to disk first. This is useful for roundtrip testing of avbroot's boot image parser. The output should be identical to the input, minus any footers, like the AVB footer.
|
||||
|
||||
### Showing information about a boot image
|
||||
|
||||
```bash
|
||||
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 cpio`
|
||||
|
||||
### Unpacking a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot cpio unpack -i <input cpio archive>
|
||||
```
|
||||
|
||||
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 cpio repack -i <input cpio archive> -o <output cpio archive>
|
||||
```
|
||||
|
||||
This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot cpio pack`, except inode numbers will not be reassigned.
|
||||
|
||||
### Showing information about a cpio archive
|
||||
|
||||
```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.
|
||||
@@ -1,242 +1,279 @@
|
||||
# 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. All Pixel devices with unlockable bootloaders since the Pixel 2 support this. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
|
||||
* NOTE: Some OnePlus devices have a broken implementation where a custom public key can be set, but the device won't boot despite having proper signatures. Downgrading the bootloader to the version shipped with Android 11 might potentially help. This problem has been reported across multiple OnePlus models ([#186](https://github.com/chenxiaolong/avbroot/issues/186), [#195](https://github.com/chenxiaolong/avbroot/discussions/195), [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
|
||||
## Patches
|
||||
|
||||
avbroot applies two patches to the boot images:
|
||||
avbroot applies the following patches to the partition images:
|
||||
|
||||
* 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). This includes the `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
|
||||
|
||||
* **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_**.
|
||||
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.
|
||||
|
||||
* 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:
|
||||
## Usage
|
||||
|
||||
* 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)).
|
||||
1. Make sure the [caveats listed above](#warnings-and-caveats) are understood. It is possible to hard brick by doing the wrong thing!
|
||||
|
||||
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
|
||||
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
|
||||
openssl genrsa 4096 | openssl pkcs8 -topk8 -scrypt -out avb.key
|
||||
openssl genrsa 4096 | openssl pkcs8 -topk8 -scrypt -out ota.key
|
||||
avbroot key generate-key -o avb.key
|
||||
avbroot key generate-key -o ota.key
|
||||
```
|
||||
|
||||
2. Convert the public key portion of the AVB signing key to the AVB public key metadata format. This is the format that the bootloader requires when setting the custom root of trust.
|
||||
|
||||
```bash
|
||||
python /path/to/avbroot/external/avb/avbtool.py extract_public_key --key avb.key --output avb_pkmd.bin
|
||||
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
|
||||
openssl req -new -x509 -sha256 -key ota.key -out ota.crt -days 10000 -subj '/CN=OTA/'
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
## Installing dependencies
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
|
||||
|
||||
avbroot depends on the `openssl` command line tool and the `lz4` and `protobuf` Python libraries. Also, Python 3.9 or newer is required.
|
||||
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.
|
||||
|
||||
### Linux
|
||||
## Initial setup
|
||||
|
||||
On Linux, the dependencies can be installed from the distro's package manager:
|
||||
|
||||
| Distro | Command |
|
||||
|------------|------------------------------------------------------------|
|
||||
| Alpine | `sudo apk add openssl py3-lz4 py3-protobuf` |
|
||||
| Arch Linux | `sudo pacman -S openssl python-lz4 python-protobuf` |
|
||||
| Debian | `sudo apt install openssl python3-lz4 python3-protobuf` |
|
||||
| Fedora | `sudo dnf install openssl python3-lz4 python3-protobuf` |
|
||||
| OpenSUSE | `sudo zypper install openssl python3-lz4 python3-protobuf` |
|
||||
| Ubuntu | (Same as Debian) |
|
||||
|
||||
### Windows
|
||||
|
||||
Installing openssl and python from the [Scoop package manager](https://scoop.sh/) is suggested.
|
||||
|
||||
```powershell
|
||||
scoop install openssl python
|
||||
```
|
||||
|
||||
Installing from other sources should work as well, but it might be necessary to manually add `openssl`'s installation directory to the `PATH` environment variable.
|
||||
|
||||
To install the Python dependencies:
|
||||
|
||||
1. Create a virtual environment (replacing `<directory>` with the path where it should be created):
|
||||
|
||||
```powershell
|
||||
python -m venv <directory>
|
||||
```
|
||||
|
||||
2. Activate the virtual environment. This must be done in every new terminal session before running avbroot.
|
||||
|
||||
```powershell
|
||||
. <directory>\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
3. Install the dependencies.
|
||||
|
||||
```powershell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
|
||||
|
||||
2. Clone this git repo recursively, as there are several AOSP repositories included as submodules in the `external/` directory.
|
||||
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/chenxiaolong/avbroot.git
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
If the repo is already cloned, run the following command instead to fetch the submodules:
|
||||
2. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
|
||||
|
||||
3. Extract the partition images from the patched OTA that are different from the original.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
3. Follow the steps to [install dependencies](#installing-dependencies).
|
||||
|
||||
4. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
5. Patch the full OTA ZIP.
|
||||
|
||||
```bash
|
||||
python avbroot.py \
|
||||
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
|
||||
```
|
||||
|
||||
If `--output` is not specified, then the output file is written to `<input>.patched`.
|
||||
|
||||
**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).
|
||||
|
||||
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).
|
||||
|
||||
6. **[Initial setup only]** Unlock the bootloader. This will trigger a data wipe.
|
||||
|
||||
7. **[Initial setup only]** Extract the patched images from the patched OTA.
|
||||
|
||||
```bash
|
||||
python avbroot.py \
|
||||
extract \
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip.patched \
|
||||
--directory extracted
|
||||
--directory extracted \
|
||||
--fastboot
|
||||
```
|
||||
|
||||
8. **[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`.
|
||||
|
||||
4. Flash the partition images that were extracted.
|
||||
|
||||
```bash
|
||||
# Flash the boot images that were extracted
|
||||
for image in extracted/*.img; do
|
||||
partition=$(basename "${image}")
|
||||
partition=${partition%.img}
|
||||
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
fastboot flash "${partition}" "${image}"
|
||||
done
|
||||
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.
|
||||
|
||||
# Flash the AVB signing public key
|
||||
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
|
||||
|
||||
5. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
|
||||
|
||||
```bash
|
||||
fastboot reboot-bootloader
|
||||
fastboot erase avb_custom_key
|
||||
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
|
||||
```
|
||||
|
||||
9. **[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:
|
||||
6. **[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
|
||||
```
|
||||
|
||||
10. **[Initial setup only]** Lock the bootloader. This will trigger a data wipe again. **Do not uncheck `OEM unlocking`!**
|
||||
7. 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.
|
||||
|
||||
8. 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 5 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 built by running:
|
||||
## Reverting to stock firmware
|
||||
|
||||
```bash
|
||||
python modules/build.py
|
||||
```
|
||||
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.
|
||||
|
||||
Alternatively, prebuilt modules can be downloaded [from GitHub Actions](https://github.com/chenxiaolong/avbroot/actions/workflows/modules.yml?query=branch%3Amaster). Select the latest workflow run and then download `avbroot-modules-<version>` at the bottom of the page. Note that GitHub only allows downloading the file when logged in.
|
||||
2. Erase the custom AVB public key.
|
||||
|
||||
### `clearotacerts`: Blocking A/B OTA Updates
|
||||
```bash
|
||||
fastboot erase avb_custom_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.
|
||||
3. Flash the stock firmware.
|
||||
|
||||
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.
|
||||
4. That's it! There are no other remnants to clean up.
|
||||
|
||||
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
|
||||
## OTA updates
|
||||
|
||||
### `oemunlockonboot`: Enable OEM unlocking 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.
|
||||
|
||||
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.
|
||||
Disabling the system updater app is recommended to prevent it from even attempting to install an unpatched OTA. To do so:
|
||||
|
||||
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
|
||||
* 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:
|
||||
|
||||
```bash
|
||||
python avbroot.py \
|
||||
extract \
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--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>
|
||||
@@ -245,8 +282,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
Alternatively, avbroot can print out what Magisk detected by running:
|
||||
|
||||
```bash
|
||||
python avbroot.py \
|
||||
magisk-info \
|
||||
avbroot boot magisk-info \
|
||||
--image magisk_patched-*.img
|
||||
```
|
||||
|
||||
@@ -256,19 +292,68 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
|
||||
If it's not possible to run the Magisk app on the target device (eg. device is currently unbootable), patch and flash the OTA once using `--ignore-magisk-warnings`, follow these steps, and then repatch and reflash the OTA with `--magisk-preinit-device <name>`.
|
||||
|
||||
## Verifying OTAs
|
||||
|
||||
To verify all signatures and hashes related to the OTA installation and AVB boot process, run:
|
||||
|
||||
```bash
|
||||
avbroot ota verify \
|
||||
--input /path/to/ota.zip \
|
||||
--cert-ota /path/to/ota.crt \
|
||||
--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
|
||||
|
||||
Since avbroot has tons of command line options, it may be useful to set up tab completions for the shell. These configs can be generated from avbroot itself.
|
||||
|
||||
#### bash
|
||||
|
||||
Add to `~/.bashrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s bash)"
|
||||
```
|
||||
|
||||
#### zsh
|
||||
|
||||
Add to `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s zsh)"
|
||||
```
|
||||
|
||||
#### fish
|
||||
|
||||
Add to `~/.config/fish/config.fish`:
|
||||
|
||||
```bash
|
||||
avbroot completion -s fish | source
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
Add to PowerShell's `profile.ps1` startup script:
|
||||
|
||||
```powershell
|
||||
Invoke-Expression (& avbroot completion -s powershell)
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### 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
|
||||
|
||||
@@ -278,12 +363,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`.
|
||||
@@ -292,21 +383,21 @@ 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 patch \
|
||||
--passphrase-avb-file /path/to/avb.passphrase \
|
||||
--passphrase-ota-file /path/to/ota.passphrase \
|
||||
avbroot ota patch \
|
||||
--pass-avb-file /path/to/avb.passphrase \
|
||||
--pass-ota-file /path/to/ota.passphrase \
|
||||
<...>
|
||||
```
|
||||
|
||||
On Unix-like systems, the "files" can be pipes. With shells that support process substituion (bash, zsh, etc.), the passphrase can be queried from a command (eg. querying a password manager).
|
||||
|
||||
```bash
|
||||
avbroot patch \
|
||||
--passphrase-avb-file <(command to query AVB passphrase) \
|
||||
--passphrase-ota-file <(command to query OTA passphrase) \
|
||||
avbroot ota patch \
|
||||
--pass-avb-file <(command to query AVB passphrase) \
|
||||
--pass-ota-file <(command to query OTA passphrase) \
|
||||
<...>
|
||||
```
|
||||
|
||||
@@ -316,41 +407,58 @@ avbroot prompts for the private key passphrases interactively by default. To run
|
||||
export PASSPHRASE_AVB="the AVB passphrase"
|
||||
export PASSPHRASE_OTA="the OTA passphrase"
|
||||
|
||||
avbroot patch \
|
||||
--passphrase-avb-env-var PASSPHRASE_AVB \
|
||||
--passphrase-ota-env-var PASSPHRASE_OTA \
|
||||
avbroot ota patch \
|
||||
--pass-avb-env-var PASSPHRASE_AVB \
|
||||
--pass-ota-env-var PASSPHRASE_OTA \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Use unencrypted private keys. This is not recommended, but can be done by:
|
||||
|
||||
```bash
|
||||
openssl pkcs8 -in avb.key -topk8 -nocrypt -out avb.unencrypted.key
|
||||
openssl pkcs8 -in ota.key -topk8 -nocrypt -out ota.unencrypted.key
|
||||
```
|
||||
* Use unencrypted private keys. This is strongly discouraged.
|
||||
|
||||
### Extracting the entire OTA
|
||||
|
||||
To extract all images contained within the OTA's `payload.bin`, run:
|
||||
|
||||
```bash
|
||||
python avbroot.py \
|
||||
extract \
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
## Building from source
|
||||
|
||||
* avbroot relies on AOSP's avbtool and OTA utilities. These are collections of applications that aren't meant to be used as libraries, but avbroot shoehorns them in anyway. These tools are not called via CLI because avbroot requires more control over the operations being performed than what is provided via the CLI interfaces. This "integration" is incredibly hacky and will likely require changes whenever the submodules are updated to point to newer AOSP commits.
|
||||
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
|
||||
|
||||
* AVB has two methods of handling signature verification:
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
* An image can have an unsigned vbmeta footer, which causes the image's hash to be embedded in the (signed) root `vbmeta` image via vbmeta hash descriptors.
|
||||
* An image can have a signed vbmeta footer, which causes a public key for verification to be embedded in the root `vbmeta` image via vbmeta chainload descriptors. This is meant for out-of-band updates where signed images can be updated without also updating the root `vbmeta` image.
|
||||
The output binary is written to `target/release/avbroot`.
|
||||
|
||||
avbroot preserves whether an image uses a chainload or hash descriptor. If a boot image was previously signed, then it will be signed with the AVB key during patching. This preserves the state of the AVB rollback indices, which makes it possible to flip between the original and patched images without a factory reset while debugging avbroot (with the bootloader unlocked).
|
||||
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
|
||||
|
||||
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`.
|
||||
|
||||
## Verifying digital signatures
|
||||
|
||||
First, save the public key to a file listing the keys to be trusted. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
|
||||
|
||||
```bash
|
||||
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
|
||||
```
|
||||
|
||||
Then, verify the signature of the zip file using the list of trusted keys.
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
|
||||
```
|
||||
|
||||
If the file is successfully verified, the output will be:
|
||||
|
||||
```
|
||||
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
# avbroot
|
||||
|
||||
avbroot – это программа для модификации OTA-образов Android A/B-формата с целью получения root-прав при сохранении прохождения AVB (Android Verified Boot) с использованием кастомных (пользовательских) ключей подписи. Она совместима как с Magisk, так и с KernelSU. При необходимости можно просто переподписать OTA, без получения root-доступа.
|
||||
|
||||
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
|
||||
|
||||
**ПРИМЕЧАНИЕ:** avbroot 2.0 была переписана на Rust и больше не имеет в основе никакого кода AOSP, а CLI полностью обратно совместим. Тем не менее, старую реализацию на Python можно найти в одноименной ветке `python`.
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
|
||||
|
||||
* наличие файла `payload.bin` (обычно находится в корне архива)
|
||||
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
|
||||
|
||||
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это делается с помощью команды `fastboot flash avb_custom_key`. Все устройства Pixel с разблокируемым загрузчиком, начиная с Pixel 2, поддерживают эту функцию. Другие устройства тоже могут поддерживать её, но достоверно убедиться в этом можно только проверив лично.
|
||||
|
||||
* ПРИМЕЧАНИЕ: Некоторые девайсы от OnePlus имеют некорректную реализацию, где возможна установка кастомного публичного ключа, но устройство все равно не будет загружаться, несмотря на наличие корректной подписи. Понижение загрузчика до версии, поставляемой вместе с Android 11, потенциально может помочь. Уже сообщалось о наличии этой проблемы на нескольких устройствах OnePlus ([#186,](https://github.com/chenxiaolong/avbroot/issues/186) [#195,](https://github.com/chenxiaolong/avbroot/discussions/195) [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
|
||||
## Патчи
|
||||
|
||||
avbroot модифицирует следующие образы:
|
||||
|
||||
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа. В случае с Magisk, патч будет эквивалентен тому, что производится в самом приложении Magisk.
|
||||
|
||||
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
|
||||
|
||||
* `system` тоже модифицируется для замены сертификата проверки подписи OTA. Это не позволит системному приложению обновлений ОС установить оригинальный непропатченный OTA и дает возможность использовать сторонние приложения для установки пропатченных OTA.
|
||||
|
||||
## Предостережения
|
||||
|
||||
* **Всегда оставляйте опцию `Заводской разблокировки`** (или OEM unlocking в англ.) **включенной при наличии root-прав с заблокированным загрузчиком.** Это очень важно. Доступ к root-правам потенциально позволяет перезаписать загрузочный раздел из-под системы, будь то сделано случайно или намеренно, файлом, который не был подписан должным образом. В таком случае, система и режим Recovery больше не смогут загрузиться, а команда `fastboot flashing unlock` будет недоступна, потому что параметр Заводской разблокировки отключен. То есть, это приведет к **_хардбрику устройства_**.
|
||||
|
||||
Повторюсь: **_ВСЕГДА оставляйте `Заводскую разблокировку` включенной при наличии root-прав._**
|
||||
|
||||
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). Это же относится и к методу `Прямой установки` для обновления Magisk. Обновление Magisk **должно выполняться только путем обновления OTA,** а не через Magisk Manager.
|
||||
|
||||
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв Issue,](https://github.com/chenxiaolong/avbroot/issues/new) и четко разъясните, какие конкретные действия привели к возникновению такой ситуации. Если Android всё еще работает и доступ к root-правам сохранился – вероятно, получится откатить изменения до исходного состояния, не стирая ваши данные.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
|
||||
|
||||
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
|
||||
|
||||
avbroot – это отдельный исполняемый файл. Он не требует установки и может быть запущен из любого места на диске.
|
||||
|
||||
3. [Сгенерируйте ключи подписи.](#генерация-ключей)
|
||||
|
||||
4. Пропатчите ОТА-архив с помощью команды:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--input /путь/к/ota.zip \
|
||||
--key-avb /путь/к/avb.key \
|
||||
--key-ota /путь/к/ota.key \
|
||||
--cert-ota /путь/к/ota.crt \
|
||||
```
|
||||
|
||||
Добавьте следующие аргументы в конец команды в зависимости от того, как вы хотите получить root-доступ.
|
||||
|
||||
* Для получения root-доступа с использованием Magisk:
|
||||
|
||||
```bash
|
||||
--magisk /путь/к/magisk.apk \
|
||||
--magisk-preinit-device <имя>
|
||||
```
|
||||
|
||||
Если вы не знаете имени раздела предварительной инициализации Magisk, следуйте инструкции [в соответствующем разделе.](#предварительная-инициализация-устройства-для-magisk)
|
||||
|
||||
Если вы пропатчили загрузочный образ вручную через приложение Magisk (вместо автоматического идентичного патчинга через avbroot), используйте следующий аргумент:
|
||||
|
||||
```bash
|
||||
--prepatched /путь/к/magisk_patched-xxxxx_yyyyy.img
|
||||
```
|
||||
|
||||
* Для получения root-доступа с использованием KernelSU:
|
||||
|
||||
```bash
|
||||
--prepatched /путь/к/kernelsu_boot.img
|
||||
```
|
||||
|
||||
* Без root-доступа:
|
||||
|
||||
```bash
|
||||
--rootless
|
||||
```
|
||||
|
||||
Больше информации про существующие аргументы можно найти в разделе [расширенного использования.](#расширенное-использование)
|
||||
|
||||
Если название для `--output` не указывается, то готовый файл будет записан как `<название-ota-zip-в-input>.patched`.
|
||||
|
||||
5. Готово! Для прошивки пропатченного OTA следуйте инструкции в разделе [первоначальной настройки.](#первоначальная-настройка) Для последующих обновлений тоже есть соответствующий [раздел обновлений.](#обновления)
|
||||
|
||||
## Генерация ключей
|
||||
|
||||
Во время патчинга OTA, avbroot подписывает несколько компонентов:
|
||||
|
||||
* загрузочный образ (boot)
|
||||
* образ vbmeta
|
||||
* payload из OTA
|
||||
* сам архив OTA
|
||||
|
||||
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
|
||||
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
|
||||
1. Сгенерируйте ключи подписи для AVB и OTA.
|
||||
|
||||
```bash
|
||||
avbroot key generate-key -o avb.key
|
||||
avbroot key generate-key -o ota.key
|
||||
```
|
||||
|
||||
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
|
||||
|
||||
```bash
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке PKCS8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
|
||||
|
||||
Если вы потеряете ключ(-и) подписи AVB или OTA, вы больше не сможете подписывать новые OTA-архивы. Придется генерировать новые ключи подписи и разблокировать загрузчик (что приведет к стиранию всех данных). В таком случае возвращайтесь к инструкции в разделе [использования.](#использование)
|
||||
|
||||
## Первоначальная настройка
|
||||
|
||||
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
|
||||
|
||||
```bash
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
2. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
|
||||
|
||||
3. Извлекаем из пропатченного OTA модифицированные образы:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip.patched \
|
||||
--directory extracted \
|
||||
--fastboot
|
||||
```
|
||||
|
||||
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
|
||||
|
||||
4. Прошейте извлеченные образы разделов.
|
||||
|
||||
```bash
|
||||
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
|
||||
|
||||
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
|
||||
|
||||
5. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
|
||||
|
||||
```bash
|
||||
fastboot reboot-bootloader
|
||||
fastboot erase avb_custom_key
|
||||
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
|
||||
```
|
||||
|
||||
6. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
|
||||
|
||||
Установите приложение Magisk или KernelSU и выполните следующую команду:
|
||||
|
||||
```bash
|
||||
adb shell su -c 'dmesg | grep libfs_avb'
|
||||
```
|
||||
|
||||
Если AVB работает корректно, будет выведено следующее сообщение:
|
||||
|
||||
```bash
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
7. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
|
||||
|
||||
```bash
|
||||
fastboot flashing lock
|
||||
```
|
||||
|
||||
Подтвердите нажатием клавиш уменьшения громкости и включения, а после перезагрузитесь в систему.
|
||||
|
||||
Напоминаю: **не отключайте `Заводскую разблокировку`!**
|
||||
|
||||
**ПРЕДУПРЕЖДЕНИЕ**: Если вы прошили CalyxOS, мастер настройки [автоматически отключит опцию `Заводской разблокировки`.](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140) Не забудьте снова включить её вручную в настройках для разработчиков. Для перестраховки можете использовать [модуль `OEMUnlockOnBoot`,](https://github.com/chenxiaolong/OEMUnlockOnBoot) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
|
||||
|
||||
8. Готово. Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
|
||||
|
||||
## Обновления
|
||||
|
||||
Обновления Android, Magisk и KernelSU выполняются одинаково – исключительно путем обновления или репатчинга того же самого OTA.
|
||||
|
||||
1. Если Magisk или KernelSU обновились, сначала установите их новый `.apk`. Если вы случайно открыли приложение после обновления, убедитесь, что оно не начало прошивать загрузочный образ. Если появится предложение обновить сам загрузочный образ – отклоните его.
|
||||
|
||||
2. Следуйте инструкции в разделе [использования,](#использование) чтобы пропатчить OTA уже с новым .apk Magisk'а/предварительно пропатченным образом с Magisk или KernelSU.
|
||||
|
||||
3. Перезагрузитесь в режим Recovery. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем нажмите кнопку увеличения громкости один раз.
|
||||
|
||||
4. Обновитесь (Apply update from adb → `adb sideload <ota.zip.patched>`).
|
||||
|
||||
5. Готово!
|
||||
|
||||
## Возврат на заводскую прошивку
|
||||
|
||||
Если вы хотите отказаться от использования avbroot и вернуться на стоковую прошивку:
|
||||
|
||||
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик. Это приведет к стиранию всех пользовательских данных.
|
||||
|
||||
2. Удалите пользовательский публичный ключ AVB.
|
||||
|
||||
```bash
|
||||
fastboot erase avb_custom_key
|
||||
```
|
||||
|
||||
3. Прошейте стоковую прошивку. Готово.
|
||||
|
||||
## OTA-обновления
|
||||
|
||||
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как при загрузке в Android, так и при сайдлоадинге через Recovery.
|
||||
|
||||
Рекомендуется отключить приложение обновлений системы, чтобы оно не пыталось установить непропатченные OTA:
|
||||
|
||||
* Стоковая прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
|
||||
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
|
||||
|
||||
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
|
||||
|
||||
Если вы хотите поднять собственный сервер для ОТА-обновлений, вам может быть интересно приложение [Custota.](https://github.com/chenxiaolong/Custota)
|
||||
|
||||
## Режим обслуживания
|
||||
|
||||
Некоторые устройства поставляются с режимом обслуживания, который загружает систему с чистым образом `userdata`, благодаря чему специалист по ремонту может проводить диагностику устройства, не имея доступа к пользовательским данным владельца.
|
||||
|
||||
Если на устройстве есть root-права, использовать этот режим небезопасно. Если у вас обычная сборка Magisk/KernelSU, не подписанная вашим собственным ключом, кто угодно может установить официальное приложение Magisk/KernelSU в режиме обслуживания и запросто получить root-права без какой-либо аутентификации.
|
||||
|
||||
Потому, чтобы безопасно использовать режим обслуживания:
|
||||
|
||||
1. Отключите root-доступ на устройстве, пропатчив OTA с аргументом `--rootless` (вместо `--magisk` или `--prepatched`) и прошив его.
|
||||
|
||||
2. Включите режим обслуживания.
|
||||
|
||||
3. Получив отремонтированное устройство обратно, выйдите из режима обслуживания.
|
||||
|
||||
4. Прошейте рутированный OTA в обычном режиме.
|
||||
|
||||
Поскольку удаление root-прав и повторное их получение выполняются путем перепрошивки OTA, данные устройства стёрты не будут.
|
||||
|
||||
## Предварительная инициализация устройства для Magisk
|
||||
|
||||
Magisk версии 25211 и новее требует наличие раздела, доступного для записи пользовательских правил SELinux, к которым необходимо обращаться на ранних этапах загрузки. Его можно определить только на реальном устройстве, поэтому avbroot требует указания точного названия с помощью аргумента `--magisk-preinit-device <имя>`. Чтобы получить имя раздела:
|
||||
|
||||
1. Извлеките загрузочный образ из оригинального, непропатченного OTA:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
```
|
||||
|
||||
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
|
||||
|
||||
Приложение Magisk выведет в лог строку, подобную следующей:
|
||||
|
||||
```
|
||||
- Pre-init storage partition device ID: <имя>
|
||||
```
|
||||
|
||||
Также и avbroot может вывести информацию о разделе, обнаруженном Magisk, для этого выполните команду:
|
||||
|
||||
```bash
|
||||
avbroot boot magisk-info \
|
||||
--image magisk_patched-*.img
|
||||
```
|
||||
|
||||
Имя раздела будет выведено как: `PREINITDEVICE=<имя>`.
|
||||
|
||||
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
|
||||
|
||||
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
|
||||
|
||||
## Проверка OTA
|
||||
|
||||
Чтобы проверить все подписи и хэши, связанные с установкой OTA и процессом загрузки AVB, выполните команду:
|
||||
|
||||
```bash
|
||||
avbroot ota verify \
|
||||
--input /путь/к/ota.zip \
|
||||
--cert-ota /путь/к/ota.crt \
|
||||
--public-key-avb /путь/к/avb_pkmd.bin
|
||||
```
|
||||
|
||||
Эта команда работает для любого OTA, независимо от того, пропатчено оно или нет.
|
||||
|
||||
Если опции `--cert-ota` и `--public-key-avb` не указаны, то подписи проверяются только на корректность, не проверяя, совпадают ли они внутри всех файлов.
|
||||
|
||||
## Подсказки через Tab
|
||||
|
||||
Поскольку avbroot имеет множество опций, будет удобно настроить подсказки с автозаполнением для используемой оболочки. Конфигурации генерируются в самом avbroot.
|
||||
|
||||
#### bash
|
||||
|
||||
Добавьте в `~/.bashrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s bash)"
|
||||
```
|
||||
|
||||
#### zsh
|
||||
|
||||
Добавьте в `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s zsh)"
|
||||
```
|
||||
|
||||
#### fish
|
||||
|
||||
Добавьте в `~/.config/fish/config.fish`:
|
||||
|
||||
```bash
|
||||
avbroot completion -s fish | source
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
Добавьте в загрузочный скрипт PowerShell (`profile.ps1`):
|
||||
|
||||
```powershell
|
||||
Invoke-Expression (& avbroot completion -s powershell)
|
||||
```
|
||||
|
||||
## Расширенное использование
|
||||
|
||||
### Использование заранее пропатченного boot.img
|
||||
|
||||
avbroot может подменить используемый загрузочный образ на заранее пропатченный (вместо того, чтобы самостоятельно применять патч). Это пригодится в случае, если у вас уже имеется пропатченный через приложение Magisk образ ядра или образ с поддержкой KernelSU. Для этого используйте аргумент `--prepatched <загрузочный образ>` вместо `--magisk <apk>`. То есть, указав `--prepatched`, avbroot пропустит применение патчинга Magisk'ом, но по-прежнему применит патч OTA-сертификата.
|
||||
|
||||
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
|
||||
|
||||
### Пропуск патчей для root-доступа
|
||||
|
||||
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
|
||||
|
||||
### Подмена разделов
|
||||
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
|
||||
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
|
||||
|
||||
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
|
||||
|
||||
### Очистка флагов vbmeta
|
||||
|
||||
Некоторые сборки Android-прошивок могут поставляться с образом `vbmeta`, в котором флаги установлены таким образом, что AVB фактически отключен. Если avbroot сталкивается с такими образом, процесс патчинга завершается ошибкой с сообщением следующего типа:
|
||||
|
||||
```
|
||||
Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
```
|
||||
|
||||
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
|
||||
|
||||
### Использование в неинтерактивном режиме
|
||||
|
||||
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
|
||||
|
||||
* Предоставить пароли через файлы:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--pass-avb-file /путь/к/avb.passphrase \
|
||||
--pass-ota-file /путь/к/ota.passphrase \
|
||||
<...>
|
||||
```
|
||||
|
||||
На Unix-подобных системах "файлы" могут быть каналами ("pipes"). В оболочках, поддерживающих подстановку процесса (bash, zsh и т. д.), пароль можно запросить с помощью команды (например, запрашивая у менеджера паролей).
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--pass-avb-file <(команда для запроса пароля AVB) \
|
||||
--pass-ota-file <(команда для запроса пароля OTA) \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Предоставить пароли через переменные среды. Это менее безопасно, поскольку любой процесс, запущенный от имени того же пользователя, может видеть значения переменных среды.
|
||||
|
||||
```bash
|
||||
export PASSPHRASE_AVB="пароль AVB"
|
||||
export PASSPHRASE_OTA="пароль OTA"
|
||||
|
||||
avbroot ota patch \
|
||||
--pass-avb-env-var PASSPHRASE_AVB \
|
||||
--pass-ota-env-var PASSPHRASE_OTA \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
|
||||
|
||||
### Извлечение всей OTA
|
||||
|
||||
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
```
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Исполняемый файл будет записан в `target/release/avbroot`.
|
||||
|
||||
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
|
||||
|
||||
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
|
||||
|
||||
## Проверка цифровых подписей
|
||||
|
||||
Сначала сохраните открытый ключ в файл, перечисляющий ключи, которым можно доверять. Это тот же ключ, который указан [в профиле автора.](https://github.com/chenxiaolong/)
|
||||
|
||||
```bash
|
||||
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
|
||||
```
|
||||
|
||||
Затем проверьте подпись zip-файла, используя список доверенных ключей.
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
|
||||
```
|
||||
|
||||
Если файл успешно проверен, вывод будет следующим:
|
||||
|
||||
```
|
||||
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
|
||||
```
|
||||
|
||||
## Вклад
|
||||
|
||||
Буду рад вашему вкладу в разработку! Однако я вряд ли приму изменения для поддержки устройств, которые ведут себя значительно иначе, чем устройства Pixel.
|
||||
|
||||
## Лицензия
|
||||
|
||||
avbroot распространяется по лицензии GPLv3. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
The changelog can be found at: [`CHANGELOG.md`](./CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
See [`README.md`](./README.md) for information on how to use avbroot.
|
||||
|
||||
The downloads are digitally signed. Please consider [verifying the digital signatures](./README.md#verifying-digital-signatures) of the binaries (or building from source) since avbroot is an application with access to your OTA/AVB signing keys.
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from avbroot import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main.main()
|
||||
@@ -0,0 +1,81 @@
|
||||
[package]
|
||||
name = "avbroot"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.4.1"
|
||||
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"
|
||||
ctrlc = "3.4.0"
|
||||
flate2 = "1.0.27"
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.3.0"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
prost = "0.12.1"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
# We use ring instead of sha2 for sha256 digest computation of large files
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
ring = "0.17.0"
|
||||
rpassword = "7.2.0"
|
||||
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.22.9", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
|
||||
# There 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/chenxiaolong/bzip2-rs"
|
||||
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
default-features = false
|
||||
features = ["deflate"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.12.1"
|
||||
protox = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["bzip2/static", "liblzma/static"]
|
||||
@@ -1,13 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
external_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)),
|
||||
'..', 'external')
|
||||
|
||||
# OTA utilities (loaded first because there are multiple common.py files and
|
||||
# this is the one we need to import)
|
||||
sys.path.append(os.path.join(external_dir, 'build', 'tools', 'releasetools'))
|
||||
# avbtool
|
||||
sys.path.append(os.path.join(external_dir, 'avb'))
|
||||
# Payload protobuf
|
||||
sys.path.append(os.path.join(external_dir, 'update_engine', 'scripts'))
|
||||
-480
@@ -1,480 +0,0 @@
|
||||
import hashlib
|
||||
import io
|
||||
import lzma
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
import avbtool
|
||||
|
||||
from . import openssl
|
||||
from . import util
|
||||
from . import vbmeta
|
||||
from .formats import bootimage
|
||||
from .formats import compression
|
||||
from .formats import cpio
|
||||
|
||||
|
||||
def _load_ramdisk(ramdisk):
|
||||
with (
|
||||
io.BytesIO(ramdisk) as f_raw,
|
||||
compression.CompressedFile(f_raw, 'rb') as f,
|
||||
):
|
||||
return cpio.load(f.fp), f.format
|
||||
|
||||
|
||||
def _save_ramdisk(entries, format):
|
||||
with io.BytesIO() as f_raw:
|
||||
with compression.CompressedFile(f_raw, 'wb', format=format) as f:
|
||||
cpio.save(f.fp, entries)
|
||||
|
||||
return f_raw.getvalue()
|
||||
|
||||
|
||||
class BootImagePatch:
|
||||
def __call__(self, image_file):
|
||||
with open(image_file, 'r+b') as f:
|
||||
boot_image = bootimage.load_autodetect(f)
|
||||
|
||||
boot_image = self.patch(image_file, boot_image)
|
||||
|
||||
f.seek(0)
|
||||
f.truncate(0)
|
||||
|
||||
boot_image.generate(f)
|
||||
|
||||
def patch(self, image_file, boot_image):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class MagiskRootPatch(BootImagePatch):
|
||||
'''
|
||||
Root the boot image with Magisk.
|
||||
'''
|
||||
|
||||
# - Half-open intervals.
|
||||
# - 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
|
||||
VERS_SUPPORTED = (
|
||||
util.Range(25102, 25207),
|
||||
util.Range(25211, 26200),
|
||||
)
|
||||
VER_PREINIT_DEVICE = util.Range(25211, VERS_SUPPORTED[-1].end)
|
||||
VER_RANDOM_SEED = util.Range(25211, VERS_SUPPORTED[-1].end)
|
||||
|
||||
def __init__(self, magisk_apk, preinit_device, random_seed):
|
||||
self.magisk_apk = magisk_apk
|
||||
self.version = self._get_version()
|
||||
|
||||
self.preinit_device = preinit_device
|
||||
|
||||
if random_seed is None:
|
||||
# Use a hardcoded random seed by default to ensure byte-for-byte
|
||||
# reproducibility
|
||||
self.random_seed = 0xfedcba9876543210
|
||||
else:
|
||||
self.random_seed = random_seed
|
||||
|
||||
def _get_version(self):
|
||||
with zipfile.ZipFile(self.magisk_apk, 'r') as z:
|
||||
with z.open('assets/util_functions.sh', 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith(b'MAGISK_VER_CODE='):
|
||||
return int(line[16:].strip())
|
||||
|
||||
raise Exception('Failed to determine Magisk version from: '
|
||||
f'{self.magisk_apk}')
|
||||
|
||||
def validate(self):
|
||||
if not any(self.version in s for s in self.VERS_SUPPORTED):
|
||||
supported = '; '.join(str(s) for s in self.VERS_SUPPORTED)
|
||||
raise ValueError(f'Unsupported Magisk version {self.version} '
|
||||
f'(supported: {supported})')
|
||||
|
||||
if self.preinit_device is None and \
|
||||
self.version in self.VER_PREINIT_DEVICE:
|
||||
raise ValueError(f'Magisk version {self.version} '
|
||||
f'({self.VER_PREINIT_DEVICE}) requires a preinit '
|
||||
f'device to be specified')
|
||||
|
||||
def patch(self, image_file, boot_image):
|
||||
with zipfile.ZipFile(self.magisk_apk, 'r') as zip:
|
||||
return self._patch(image_file, boot_image, zip)
|
||||
|
||||
def _patch(self, image_file, boot_image, zip):
|
||||
if len(boot_image.ramdisks) > 1:
|
||||
raise Exception('Boot image is not expected to have '
|
||||
f'{len(boot_image.ramdisks)} ramdisks')
|
||||
|
||||
# Magisk saves the original SHA1 digest in its config file
|
||||
with open(image_file, 'rb') as f:
|
||||
hasher = util.hash_file(f, hashlib.sha1())
|
||||
|
||||
# Load the existing ramdisk if it exists. If it doesn't, we have to
|
||||
# generate one from scratch
|
||||
if boot_image.ramdisks:
|
||||
entries, ramdisk_format = _load_ramdisk(boot_image.ramdisks[0])
|
||||
else:
|
||||
entries, ramdisk_format = [], compression.Format.LZ4_LEGACY
|
||||
|
||||
old_entries = entries.copy()
|
||||
|
||||
# Create magisk directory structure
|
||||
for path, perms in (
|
||||
(b'overlay.d', 0o750),
|
||||
(b'overlay.d/sbin', 0o750),
|
||||
):
|
||||
entries.append(cpio.CpioEntryNew.new_directory(path, perms=perms))
|
||||
|
||||
# Delete the original init
|
||||
if boot_image.ramdisks:
|
||||
entries = [e for e in entries if e.name != b'init']
|
||||
|
||||
# Add magiskinit
|
||||
with zip.open('lib/arm64-v8a/libmagiskinit.so', 'r') as f:
|
||||
entries.append(cpio.CpioEntryNew.new_file(
|
||||
b'init', perms=0o750, data=f.read()))
|
||||
|
||||
# Add xz-compressed magisk32 and magisk64
|
||||
xz_files = {
|
||||
'lib/armeabi-v7a/libmagisk32.so': b'magisk32.xz',
|
||||
'lib/arm64-v8a/libmagisk64.so': b'magisk64.xz',
|
||||
}
|
||||
|
||||
# Add stub apk, which only exists after the Magisk commit:
|
||||
# ad0e6511e11ebec65aa9b5b916e1397342850319
|
||||
if 'assets/stub.apk' in zip.namelist():
|
||||
xz_files['assets/stub.apk'] = b'stub.xz'
|
||||
|
||||
for source, target in xz_files.items():
|
||||
with (
|
||||
zip.open(source, 'r') as f_in,
|
||||
io.BytesIO() as f_out_raw,
|
||||
):
|
||||
with lzma.open(f_out_raw, 'wb', preset=9,
|
||||
check=lzma.CHECK_CRC32) as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
entries.append(cpio.CpioEntryNew.new_file(
|
||||
b'overlay.d/sbin/' + target, perms=0o644,
|
||||
data=f_out_raw.getvalue()))
|
||||
|
||||
# Create magisk .backup directory structure
|
||||
self._apply_magisk_backup(old_entries, entries)
|
||||
|
||||
# Create magisk config
|
||||
magisk_config = \
|
||||
b'KEEPVERITY=true\n' \
|
||||
b'KEEPFORCEENCRYPT=true\n' \
|
||||
b'PATCHVBMETAFLAG=false\n' \
|
||||
b'RECOVERYMODE=false\n'
|
||||
|
||||
if self.version in self.VER_PREINIT_DEVICE:
|
||||
magisk_config += b'PREINITDEVICE=%s\n' % \
|
||||
self.preinit_device.encode('ascii')
|
||||
|
||||
magisk_config += b'SHA1=%s\n' % hasher.hexdigest().encode('ascii')
|
||||
|
||||
if self.version in self.VER_RANDOM_SEED:
|
||||
magisk_config += b'RANDOMSEED=0x%x\n' % self.random_seed
|
||||
|
||||
entries.append(cpio.CpioEntryNew.new_file(
|
||||
b'.backup/.magisk', perms=0o000, data=magisk_config))
|
||||
|
||||
# Repack ramdisk
|
||||
new_ramdisk = _save_ramdisk(entries, ramdisk_format)
|
||||
if boot_image.ramdisks:
|
||||
boot_image.ramdisks[0] = new_ramdisk
|
||||
else:
|
||||
boot_image.ramdisks.append(new_ramdisk)
|
||||
|
||||
return boot_image
|
||||
|
||||
@staticmethod
|
||||
def _apply_magisk_backup(old_entries, new_entries):
|
||||
'''
|
||||
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.
|
||||
'''
|
||||
|
||||
old_by_name = {e.name: e for e in old_entries}
|
||||
new_by_name = {e.name: e for e in new_entries}
|
||||
|
||||
added = new_by_name.keys() - old_by_name.keys()
|
||||
deleted = old_by_name.keys() - new_by_name.keys()
|
||||
changed = set(n for n in old_by_name.keys() & new_by_name.keys()
|
||||
if old_by_name[n].content != new_by_name[n].content)
|
||||
|
||||
new_entries.append(cpio.CpioEntryNew.new_directory(
|
||||
b'.backup', perms=0o000))
|
||||
|
||||
for name in deleted | changed:
|
||||
entry = old_by_name[name]
|
||||
entry.name = b'.backup/' + entry.name
|
||||
new_entries.append(entry)
|
||||
|
||||
rmlist_data = b''.join(n + b'\0' for n in sorted(added))
|
||||
new_entries.append(cpio.CpioEntryNew.new_file(
|
||||
b'.backup/.rmlist', perms=0o000, data=rmlist_data))
|
||||
|
||||
|
||||
class OtaCertPatch(BootImagePatch):
|
||||
'''
|
||||
Replace the OTA certificates in the vendor_boot image with the custom OTA
|
||||
signing certificate.
|
||||
'''
|
||||
|
||||
OTACERTS_PATH = b'system/etc/security/otacerts.zip'
|
||||
|
||||
def __init__(self, cert_ota):
|
||||
self.cert_ota = cert_ota
|
||||
|
||||
def patch(self, image_file, boot_image):
|
||||
found_otacerts = False
|
||||
|
||||
# Check each ramdisk
|
||||
for i, ramdisk in enumerate(boot_image.ramdisks):
|
||||
entries, ramdisk_format = _load_ramdisk(ramdisk)
|
||||
|
||||
# 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.
|
||||
otacerts = next((e for e in entries if e.name ==
|
||||
self.OTACERTS_PATH), None)
|
||||
if otacerts:
|
||||
found_otacerts = True
|
||||
else:
|
||||
continue
|
||||
|
||||
# Create new otacerts archive. The old certs are ignored since
|
||||
# flashing a stock OTA will render the device unbootable.
|
||||
with io.BytesIO() as f_zip:
|
||||
with zipfile.ZipFile(f_zip, 'w') as z:
|
||||
# Use zeroed-out metadata to ensure the archive is bit for
|
||||
# bit reproducible across runs.
|
||||
info = zipfile.ZipInfo('ota.x509.pem')
|
||||
# Mark entry as created on Unix for reproducibility
|
||||
info.create_system = 3
|
||||
with (
|
||||
z.open(info, 'w') as f_out,
|
||||
open(self.cert_ota, 'rb') as f_in,
|
||||
):
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
otacerts.content = f_zip.getvalue()
|
||||
|
||||
# Repack ramdisk
|
||||
boot_image.ramdisks[i] = _save_ramdisk(entries, ramdisk_format)
|
||||
|
||||
if not found_otacerts:
|
||||
raise Exception(f'{self.OTACERTS_PATH} not found in ramdisk')
|
||||
|
||||
return boot_image
|
||||
|
||||
|
||||
class PrepatchedImage(BootImagePatch):
|
||||
'''
|
||||
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.
|
||||
'''
|
||||
|
||||
MIN_LEVEL = 0
|
||||
MAX_LEVEL = 2
|
||||
|
||||
VERSION_REGEX = re.compile(
|
||||
b'Linux version (\d+\.\d+).\d+-(android\d+)-(\d+)-')
|
||||
|
||||
def __init__(self, prepatched, fatal_level, warning_fn):
|
||||
self.prepatched = prepatched
|
||||
self.fatal_level = fatal_level
|
||||
self.warning_fn = warning_fn
|
||||
|
||||
def patch(self, image_file, boot_image):
|
||||
with open(self.prepatched, 'r+b') as f:
|
||||
prepatched_image = bootimage.load_autodetect(f)
|
||||
|
||||
old_header = boot_image.to_dict()
|
||||
new_header = prepatched_image.to_dict()
|
||||
|
||||
# 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
|
||||
issues = [[], [], []]
|
||||
|
||||
for k in new_header.keys() - old_header.keys():
|
||||
issues[2].append(f'{k} header field was added')
|
||||
|
||||
for k in old_header.keys() - new_header.keys():
|
||||
issues[2].append(f'{k} header field was removed')
|
||||
|
||||
for k in old_header.keys() & new_header.keys():
|
||||
if old_header[k] != new_header[k]:
|
||||
if k in ('id', 'os_version'):
|
||||
level = 0
|
||||
elif k in ('cmdline', 'extra_cmdline'):
|
||||
level = 1
|
||||
else:
|
||||
level = 2
|
||||
|
||||
issues[level].append(f'{k} header field was changed: '
|
||||
f'{old_header[k]} -> {new_header[k]}')
|
||||
|
||||
for attr in 'kernel', 'second', 'recovery_dtbo', 'dtb', 'bootconfig':
|
||||
original_val = getattr(boot_image, attr)
|
||||
prepatched_val = getattr(prepatched_image, attr)
|
||||
|
||||
if original_val is None and prepatched_val is not None:
|
||||
issues[1].append(f'{attr} section was added')
|
||||
elif original_val is not None and prepatched_val is None:
|
||||
issues[2].append(f'{attr} section was removed')
|
||||
|
||||
if len(prepatched_image.ramdisks) < len(boot_image.ramdisks):
|
||||
issues[2].append('Number of ramdisk sections decreased: '
|
||||
f'{len(boot_image.ramdisks)} -> '
|
||||
f'{len(prepatched_image.ramdisks)}')
|
||||
|
||||
if boot_image.kernel is not None:
|
||||
old_kmi = self._get_kmi_version(boot_image)
|
||||
new_kmi = self._get_kmi_version(prepatched_image)
|
||||
|
||||
if old_kmi != new_kmi:
|
||||
issues[2].append('Kernel module interface version changed: '
|
||||
f'{old_kmi} -> {new_kmi}')
|
||||
|
||||
warnings = [e for i in range(self.MIN_LEVEL,
|
||||
min(self.MAX_LEVEL + 1, self.fatal_level))
|
||||
for e in issues[i]]
|
||||
errors = [e for i in range(max(self.MIN_LEVEL, self.fatal_level),
|
||||
self.MAX_LEVEL + 1)
|
||||
for e in issues[i]]
|
||||
|
||||
if warnings:
|
||||
self.warning_fn('The prepatched boot image may not be compatible '
|
||||
'with the original:\n' +
|
||||
'\n'.join(f'- {w}' for w in warnings))
|
||||
|
||||
if errors:
|
||||
raise ValueError('The prepatched boot image is not compatible '
|
||||
'with the original:\n' +
|
||||
'\n'.join(f'- {e}' for e in errors))
|
||||
|
||||
return prepatched_image
|
||||
|
||||
@classmethod
|
||||
def _get_kmi_version(cls, boot_image):
|
||||
try:
|
||||
with (
|
||||
io.BytesIO(boot_image.kernel) as f_raw,
|
||||
compression.CompressedFile(f_raw, 'rb') as f,
|
||||
):
|
||||
decompressed = f.fp.read()
|
||||
except ValueError:
|
||||
decompressed = boot_image.kernel
|
||||
|
||||
m = cls.VERSION_REGEX.search(decompressed)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
return b'-'.join(m.groups()).decode('ascii')
|
||||
|
||||
|
||||
def patch_boot(avb, input_path, output_path, key, passphrase,
|
||||
only_if_previously_signed, patch_funcs):
|
||||
'''
|
||||
Call each function in patch_funcs against a boot image with vbmeta stripped
|
||||
out and then resign the image using the provided private key.
|
||||
'''
|
||||
|
||||
image = avbtool.ImageHandler(input_path, read_only=True)
|
||||
footer, header, descriptors, image_size = avb._parse_image(image)
|
||||
|
||||
have_key_old = not not header.public_key_size
|
||||
if not have_key_old and only_if_previously_signed:
|
||||
key = None
|
||||
|
||||
have_key_new = not not key
|
||||
|
||||
if have_key_old != have_key_new:
|
||||
raise Exception('Key presence does not match: %s (old) != %s (new)' %
|
||||
(have_key_old, have_key_new))
|
||||
|
||||
hash = None
|
||||
new_descriptors = []
|
||||
|
||||
for d in descriptors:
|
||||
if isinstance(d, avbtool.AvbHashDescriptor):
|
||||
if hash is not None:
|
||||
raise Exception('Expected only one hash descriptor')
|
||||
hash = d
|
||||
else:
|
||||
new_descriptors.append(d)
|
||||
|
||||
if hash is None:
|
||||
raise Exception('No hash descriptor found')
|
||||
|
||||
algorithm_name = avbtool.lookup_algorithm_by_type(header.algorithm_type)[0]
|
||||
|
||||
# Pixel 7's init_boot image is originally signed by a 2048-bit RSA key, but
|
||||
# avbroot expects RSA 4096 keys
|
||||
if algorithm_name == 'SHA256_RSA2048':
|
||||
algorithm_name = 'SHA256_RSA4096'
|
||||
|
||||
with util.open_output_file(output_path) as f:
|
||||
shutil.copyfile(input_path, f.name)
|
||||
|
||||
# Strip the vbmeta footer from the boot image
|
||||
avb.erase_footer(f.name, False)
|
||||
|
||||
# Invoke the patching functions
|
||||
for patch_func in patch_funcs:
|
||||
patch_func(f.name)
|
||||
|
||||
# Sign the new boot image
|
||||
with (
|
||||
vbmeta.smuggle_descriptors(),
|
||||
openssl.inject_passphrase(passphrase),
|
||||
):
|
||||
avb.add_hash_footer(
|
||||
image_filename=f.name,
|
||||
partition_size=image_size,
|
||||
dynamic_partition_size=False,
|
||||
partition_name=hash.partition_name,
|
||||
hash_algorithm=hash.hash_algorithm,
|
||||
salt=hash.salt.hex(),
|
||||
chain_partitions=None,
|
||||
algorithm_name=algorithm_name,
|
||||
key_path=key,
|
||||
public_key_metadata_path=None,
|
||||
rollback_index=header.rollback_index,
|
||||
flags=header.flags,
|
||||
rollback_index_location=header.rollback_index_location,
|
||||
props=None,
|
||||
props_from_file=None,
|
||||
kernel_cmdlines=new_descriptors,
|
||||
setup_rootfs_from_kernel=None,
|
||||
include_descriptors_from_image=None,
|
||||
calc_max_image_size=False,
|
||||
signing_helper=None,
|
||||
signing_helper_with_files=None,
|
||||
release_string=header.release_string,
|
||||
append_to_release_string=None,
|
||||
output_vbmeta_image=None,
|
||||
do_not_append_vbmeta_image=False,
|
||||
print_required_libavb_version=False,
|
||||
use_persistent_digest=False,
|
||||
do_not_use_ab=False,
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{env, ffi::OsStr, fs, io, path::Path};
|
||||
|
||||
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");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", in_dir.to_str().unwrap());
|
||||
|
||||
let mut protos = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(&in_dir).unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
if path.extension() == Some(OsStr::new("proto")) {
|
||||
println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
|
||||
protos.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
match fs::remove_dir_all(&out_dir) {
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
||||
r => r.unwrap(),
|
||||
}
|
||||
|
||||
fs::create_dir_all(&out_dir).unwrap();
|
||||
|
||||
let file_descriptors = protox::compile(&protos, [&in_dir]).unwrap();
|
||||
|
||||
prost_build::Config::new()
|
||||
.btree_map(["."])
|
||||
.compile_fds(file_descriptors)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,771 +0,0 @@
|
||||
import collections
|
||||
import os
|
||||
import struct
|
||||
import typing
|
||||
|
||||
from . import padding
|
||||
from .. import util
|
||||
|
||||
|
||||
BOOT_MAGIC = b'ANDROID!'
|
||||
BOOT_NAME_SIZE = 16
|
||||
BOOT_ARGS_SIZE = 512
|
||||
BOOT_EXTRA_ARGS_SIZE = 1024
|
||||
|
||||
VENDOR_BOOT_MAGIC = b'VNDRBOOT'
|
||||
VENDOR_BOOT_ARGS_SIZE = 2048
|
||||
VENDOR_BOOT_NAME_SIZE = 16
|
||||
|
||||
VENDOR_RAMDISK_TYPE_NONE = 0
|
||||
VENDOR_RAMDISK_TYPE_PLATFORM = 1
|
||||
VENDOR_RAMDISK_TYPE_RECOVERY = 2
|
||||
VENDOR_RAMDISK_TYPE_DLKM = 3
|
||||
VENDOR_RAMDISK_NAME_SIZE = 32
|
||||
VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE = 16
|
||||
|
||||
PAGE_SIZE = 4096
|
||||
|
||||
BOOT_IMG_HDR_V0 = struct.Struct(
|
||||
'<'
|
||||
f'{len(BOOT_MAGIC)}s' # magic
|
||||
'I' # kernel_size
|
||||
'I' # kernel_addr
|
||||
'I' # ramdisk_size
|
||||
'I' # ramdisk_addr
|
||||
'I' # second_size
|
||||
'I' # second_addr
|
||||
'I' # tags_addr
|
||||
'I' # page_size
|
||||
'I' # header_version
|
||||
'I' # os_version
|
||||
f'{BOOT_NAME_SIZE}s' # name
|
||||
f'{BOOT_ARGS_SIZE}s' # cmdline
|
||||
f'{8 * 4}s' # id (uint32_t[8])
|
||||
f'{BOOT_EXTRA_ARGS_SIZE}s' # extra_cmdline
|
||||
)
|
||||
|
||||
BOOT_IMG_HDR_V1_EXTRA = struct.Struct(
|
||||
'<'
|
||||
'I' # recovery_dtbo_size
|
||||
'Q' # recovery_dtbo_offset
|
||||
'I' # header_size
|
||||
)
|
||||
|
||||
BOOT_IMG_HDR_V2_EXTRA = struct.Struct(
|
||||
'<'
|
||||
'I' # dtb_size
|
||||
'Q' # dtb_addr
|
||||
)
|
||||
|
||||
BOOT_IMG_HDR_V3 = struct.Struct(
|
||||
'<'
|
||||
f'{len(BOOT_MAGIC)}s' # magic
|
||||
'I' # kernel_size
|
||||
'I' # ramdisk_size
|
||||
'I' # os_version
|
||||
'I' # header_size
|
||||
'16s' # reserved (uint32_t[4])
|
||||
'I' # header_version
|
||||
f'{BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE}s' # cmdline
|
||||
)
|
||||
|
||||
VENDOR_BOOT_IMG_HDR_V3 = struct.Struct(
|
||||
'<'
|
||||
f'{len(VENDOR_BOOT_MAGIC)}s' # magic
|
||||
'I' # header_version
|
||||
'I' # page_size
|
||||
'I' # kernel_addr
|
||||
'I' # ramdisk_addr
|
||||
'I' # vendor_ramdisk_size
|
||||
f'{VENDOR_BOOT_ARGS_SIZE}s' # cmdline
|
||||
'I' # tags_addr
|
||||
f'{VENDOR_BOOT_NAME_SIZE}s' # name
|
||||
'I' # header_size
|
||||
'I' # dtb_size
|
||||
'Q' # dtb_addr
|
||||
)
|
||||
|
||||
BOOT_IMG_HDR_V4_EXTRA = struct.Struct(
|
||||
'<'
|
||||
'I' # signature_size
|
||||
)
|
||||
|
||||
VENDOR_BOOT_IMG_HDR_V4_EXTRA = struct.Struct(
|
||||
'<'
|
||||
'I' # vendor_ramdisk_table_size
|
||||
'I' # vendor_ramdisk_table_entry_num
|
||||
'I' # vendor_ramdisk_table_entry_size
|
||||
'I' # bootconfig_size
|
||||
)
|
||||
|
||||
VENDOR_RAMDISK_TABLE_ENTRY_V4 = struct.Struct(
|
||||
'<'
|
||||
'I' # ramdisk_size
|
||||
'I' # ramdisk_offset
|
||||
'I' # ramdisk_type
|
||||
f'{VENDOR_RAMDISK_NAME_SIZE}s' # ramdisk_name
|
||||
f'{VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE * 4}s' # board_id (uint32_t[])
|
||||
)
|
||||
|
||||
|
||||
class WrongFormat(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class BootImage:
|
||||
def __init__(
|
||||
self,
|
||||
f: typing.Optional[typing.BinaryIO] = None,
|
||||
data: typing.Optional[dict[str, typing.Any]] = None,
|
||||
) -> None:
|
||||
assert (f is None) != (data is None)
|
||||
|
||||
self.kernel: typing.Optional[bytes] = None
|
||||
self.ramdisks: list[bytes] = []
|
||||
self.second: typing.Optional[bytes] = None
|
||||
self.recovery_dtbo: typing.Optional[bytes] = None
|
||||
self.dtb: typing.Optional[bytes] = None
|
||||
self.bootconfig: typing.Optional[bytes] = None
|
||||
|
||||
if f:
|
||||
self._from_file(f)
|
||||
else:
|
||||
self._from_dict(data)
|
||||
|
||||
def _from_file(self, f: typing.BinaryIO) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate(self, f: typing.BinaryIO) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _from_dict(self, data: dict[str, typing.Any]) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def to_dict(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class _BootImageV0Through2(BootImage):
|
||||
def _from_file(self, f: typing.BinaryIO) -> None:
|
||||
# Common fields for v0 through v2
|
||||
magic, kernel_size, kernel_addr, ramdisk_size, ramdisk_addr, \
|
||||
second_size, second_addr, tags_addr, page_size, header_version, \
|
||||
os_version, name, cmdline, id, extra_cmdline = \
|
||||
BOOT_IMG_HDR_V0.unpack(util.read_exact(f, BOOT_IMG_HDR_V0.size))
|
||||
|
||||
if magic != BOOT_MAGIC:
|
||||
raise WrongFormat(f'Unknown magic: {magic}')
|
||||
elif header_version not in (0, 1, 2):
|
||||
raise WrongFormat(f'Unknown header version: {header_version}')
|
||||
|
||||
self.kernel_addr = kernel_addr
|
||||
self.ramdisk_addr = ramdisk_addr
|
||||
self.second_addr = second_addr
|
||||
self.tags_addr = tags_addr
|
||||
self.page_size = page_size
|
||||
self.header_version = header_version
|
||||
self.os_version = os_version
|
||||
self.name = name.rstrip(b'\0')
|
||||
self.cmdline = cmdline.rstrip(b'\0')
|
||||
self.id = id
|
||||
self.extra_cmdline = extra_cmdline.rstrip(b'\0')
|
||||
|
||||
# Parse v1 fields
|
||||
if header_version >= 1:
|
||||
recovery_dtbo_size, recovery_dtbo_offset, header_size = \
|
||||
BOOT_IMG_HDR_V1_EXTRA.unpack(
|
||||
util.read_exact(f, BOOT_IMG_HDR_V1_EXTRA.size))
|
||||
|
||||
self.recovery_dtbo_offset = recovery_dtbo_offset
|
||||
|
||||
# Parse v2 fields
|
||||
if header_version == 2:
|
||||
dtb_size, dtb_addr = BOOT_IMG_HDR_V2_EXTRA.unpack(
|
||||
util.read_exact(f, BOOT_IMG_HDR_V2_EXTRA.size))
|
||||
|
||||
self.dtb_addr = dtb_addr
|
||||
|
||||
if header_version >= 1 and f.tell() != header_size:
|
||||
raise ValueError(f'Invalid header size: {header_size}')
|
||||
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if kernel_size > 0:
|
||||
self.kernel = util.read_exact(f, kernel_size)
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if ramdisk_size > 0:
|
||||
self.ramdisks.append(util.read_exact(f, ramdisk_size))
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if second_size > 0:
|
||||
self.second = util.read_exact(f, second_size)
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if header_version >= 1 and recovery_dtbo_size > 0:
|
||||
self.recovery_dtbo = util.read_exact(f, recovery_dtbo_size)
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if header_version == 2 and dtb_size > 0:
|
||||
self.dtb = util.read_exact(f, dtb_size)
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
def generate(self, f: typing.BinaryIO) -> None:
|
||||
if len(self.ramdisks) > 1:
|
||||
raise ValueError('Only one ramdisk is supported')
|
||||
elif self.bootconfig is not None:
|
||||
raise ValueError('Boot config is not supported')
|
||||
elif self.header_version < 1 and self.recovery_dtbo is not None:
|
||||
raise ValueError('Recovery dtbo/acpio is not supported')
|
||||
elif self.header_version < 2 and self.dtb is not None:
|
||||
raise ValueError('Device tree is not supported')
|
||||
|
||||
f.write(BOOT_IMG_HDR_V0.pack(
|
||||
BOOT_MAGIC,
|
||||
len(self.kernel) if self.kernel else 0,
|
||||
self.kernel_addr,
|
||||
len(self.ramdisks[0]) if self.ramdisks else 0,
|
||||
self.ramdisk_addr,
|
||||
len(self.second) if self.second else 0,
|
||||
self.second_addr,
|
||||
self.tags_addr,
|
||||
self.page_size,
|
||||
self.header_version,
|
||||
self.os_version,
|
||||
self.name,
|
||||
self.cmdline,
|
||||
self.id,
|
||||
self.extra_cmdline,
|
||||
))
|
||||
|
||||
if self.header_version >= 1:
|
||||
header_size = BOOT_IMG_HDR_V0.size
|
||||
if self.header_version >= 1:
|
||||
header_size += BOOT_IMG_HDR_V1_EXTRA.size
|
||||
if self.header_version == 2:
|
||||
header_size += BOOT_IMG_HDR_V2_EXTRA.size
|
||||
|
||||
f.write(BOOT_IMG_HDR_V1_EXTRA.pack(
|
||||
len(self.recovery_dtbo) if self.recovery_dtbo else 0,
|
||||
self.recovery_dtbo_offset,
|
||||
header_size,
|
||||
))
|
||||
|
||||
if self.header_version == 2:
|
||||
f.write(BOOT_IMG_HDR_V2_EXTRA.pack(
|
||||
len(self.dtb) if self.dtb else 0,
|
||||
self.dtb_addr,
|
||||
))
|
||||
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.kernel:
|
||||
f.write(self.kernel)
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.ramdisks:
|
||||
f.write(self.ramdisks[0])
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.second:
|
||||
f.write(self.second)
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.header_version >= 1 and self.recovery_dtbo:
|
||||
f.write(self.recovery_dtbo)
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.header_version == 2 and self.dtb:
|
||||
f.write(self.dtb)
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
def __str__(self) -> str:
|
||||
kernel_size = len(self.kernel) if self.kernel else 0
|
||||
ramdisk_size = len(self.ramdisks[0]) if self.ramdisks else 0
|
||||
second_size = len(self.second) if self.second else 0
|
||||
|
||||
result = \
|
||||
f'Boot image v{self.header_version} header:\n' \
|
||||
f'- Kernel size: {kernel_size}\n' \
|
||||
f'- Kernel address: 0x{self.kernel_addr:x}\n' \
|
||||
f'- Ramdisk size: {ramdisk_size}\n' \
|
||||
f'- Ramdisk address: 0x{self.ramdisk_addr:x}\n' \
|
||||
f'- Second stage size: {second_size}\n' \
|
||||
f'- Second stage address: 0x{self.second_addr:x}\n' \
|
||||
f'- Kernel tags address: 0x{self.tags_addr:x}\n' \
|
||||
f'- Page size: {self.page_size}\n' \
|
||||
f'- OS version: 0x{self.os_version:x}\n' \
|
||||
f'- Name: {self.name!r}\n' \
|
||||
f'- Kernel cmdline: {self.cmdline!r}\n' \
|
||||
f'- ID: {self.id.hex()}\n' \
|
||||
f'- Extra kernel cmdline: {self.extra_cmdline!r}\n'
|
||||
|
||||
if self.header_version >= 1:
|
||||
recovery_dtbo_size = len(self.recovery_dtbo) \
|
||||
if self.recovery_dtbo else 0
|
||||
|
||||
result += \
|
||||
f'- Recovery dtbo size: {recovery_dtbo_size}\n' \
|
||||
f'- Recovery dtbo offset: {self.recovery_dtbo_offset}\n'
|
||||
|
||||
if self.header_version == 2:
|
||||
dtb_size = len(self.dtb) if self.dtb else 0
|
||||
|
||||
result += \
|
||||
f'- Device tree size: {dtb_size}\n' \
|
||||
f'- Device tree address: {self.dtb_addr}\n'
|
||||
|
||||
return result
|
||||
|
||||
def _from_dict(self, data: dict[str, typing.Any]) -> None:
|
||||
type = data.get('type')
|
||||
header_version = data.get('header_version')
|
||||
|
||||
if type != 'android':
|
||||
raise WrongFormat(f'Unknown type: {type}')
|
||||
elif header_version not in (0, 1, 2):
|
||||
raise WrongFormat(f'Unknown header version: {header_version}')
|
||||
|
||||
self.header_version = header_version
|
||||
self.kernel_addr = data['kernel_address']
|
||||
self.ramdisk_addr = data['ramdisk_address']
|
||||
self.second_addr = data['second_address']
|
||||
self.tags_addr = data['tags_address']
|
||||
self.page_size = data['page_size']
|
||||
self.os_version = data['os_version']
|
||||
self.name = data['name']
|
||||
self.cmdline = data['cmdline']
|
||||
self.id = data['id']
|
||||
self.extra_cmdline = data['extra_cmdline']
|
||||
|
||||
if header_version >= 1:
|
||||
self.recovery_dtbo_offset = data['recovery_dtbo_offset']
|
||||
|
||||
if self.header_version == 2:
|
||||
self.dtb_addr = data['dtb_address']
|
||||
|
||||
def to_dict(self) -> dict[str, typing.Any]:
|
||||
result = {
|
||||
'type': 'android',
|
||||
'header_version': self.header_version,
|
||||
'kernel_address': self.kernel_addr,
|
||||
'ramdisk_address': self.ramdisk_addr,
|
||||
'second_address': self.second_addr,
|
||||
'tags_address': self.tags_addr,
|
||||
'page_size': self.page_size,
|
||||
'os_version': self.os_version,
|
||||
'name': self.name,
|
||||
'cmdline': self.cmdline,
|
||||
'id': self.id,
|
||||
'extra_cmdline': self.extra_cmdline,
|
||||
}
|
||||
|
||||
if self.header_version >= 1:
|
||||
result['recovery_dtbo_offset'] = self.recovery_dtbo_offset
|
||||
|
||||
if self.header_version == 2:
|
||||
result['dtb_address'] = self.dtb_addr
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _BootImageV3Through4(BootImage):
|
||||
def _from_file(self, f: typing.BinaryIO) -> None:
|
||||
# Common fields for both v3 and v4
|
||||
magic, kernel_size, ramdisk_size, os_version, header_size, reserved, \
|
||||
header_version, cmdline = BOOT_IMG_HDR_V3.unpack(
|
||||
util.read_exact(f, BOOT_IMG_HDR_V3.size))
|
||||
|
||||
if magic != BOOT_MAGIC:
|
||||
raise WrongFormat(f'Unknown magic: {magic}')
|
||||
elif header_version not in (3, 4):
|
||||
raise WrongFormat(f'Unknown header version: {header_version}')
|
||||
|
||||
# Parse v4 fields
|
||||
if header_version == 4:
|
||||
signature_size, = BOOT_IMG_HDR_V4_EXTRA.unpack(
|
||||
util.read_exact(f, BOOT_IMG_HDR_V4_EXTRA.size))
|
||||
|
||||
if f.tell() != header_size:
|
||||
raise ValueError(f'Invalid header size: {header_size}')
|
||||
|
||||
self.header_version = header_version
|
||||
self.os_version = os_version
|
||||
self.reserved = reserved
|
||||
self.cmdline = cmdline.rstrip(b'\0')
|
||||
|
||||
padding.read_skip(f, PAGE_SIZE)
|
||||
|
||||
if kernel_size > 0:
|
||||
self.kernel = util.read_exact(f, kernel_size)
|
||||
padding.read_skip(f, PAGE_SIZE)
|
||||
|
||||
if ramdisk_size > 0:
|
||||
self.ramdisks.append(util.read_exact(f, ramdisk_size))
|
||||
padding.read_skip(f, PAGE_SIZE)
|
||||
|
||||
if header_version == 4:
|
||||
# Don't preserve the signature. It is only used for VTS tests and
|
||||
# is not relevant for booting
|
||||
f.seek(signature_size, os.SEEK_CUR)
|
||||
padding.read_skip(f, PAGE_SIZE)
|
||||
|
||||
def generate(self, f: typing.BinaryIO) -> None:
|
||||
if len(self.ramdisks) > 1:
|
||||
raise ValueError('Only one ramdisk is supported')
|
||||
elif self.second is not None:
|
||||
raise ValueError('Second stage bootloader is not supported')
|
||||
elif self.recovery_dtbo is not None:
|
||||
raise ValueError('Recovery dtbo/acpio is not supported')
|
||||
elif self.dtb is not None:
|
||||
raise ValueError('Device tree is not supported')
|
||||
elif self.bootconfig is not None:
|
||||
raise ValueError('Boot config is not supported')
|
||||
|
||||
f.write(BOOT_IMG_HDR_V3.pack(
|
||||
BOOT_MAGIC,
|
||||
len(self.kernel) if self.kernel else 0,
|
||||
len(self.ramdisks[0]) if self.ramdisks else 0,
|
||||
self.os_version,
|
||||
BOOT_IMG_HDR_V3.size + (BOOT_IMG_HDR_V4_EXTRA.size
|
||||
if self.header_version == 4 else 0),
|
||||
self.reserved,
|
||||
self.header_version,
|
||||
self.cmdline,
|
||||
))
|
||||
|
||||
if self.header_version == 4:
|
||||
f.write(BOOT_IMG_HDR_V4_EXTRA.pack(
|
||||
# We don't care about the VTS signature
|
||||
0
|
||||
))
|
||||
|
||||
padding.write(f, PAGE_SIZE)
|
||||
|
||||
if self.kernel:
|
||||
f.write(self.kernel)
|
||||
padding.write(f, PAGE_SIZE)
|
||||
|
||||
if self.ramdisks:
|
||||
f.write(self.ramdisks[0])
|
||||
padding.write(f, PAGE_SIZE)
|
||||
|
||||
def __str__(self) -> str:
|
||||
kernel_size = len(self.kernel) if self.kernel else 0
|
||||
ramdisk_size = len(self.ramdisks[0]) if self.ramdisks else 0
|
||||
|
||||
return \
|
||||
f'Boot image v{self.header_version} header:\n' \
|
||||
f'- Kernel size: {kernel_size}\n' \
|
||||
f'- Ramdisk size: {ramdisk_size}\n' \
|
||||
f'- OS version: 0x{self.os_version:x}\n' \
|
||||
f'- Reserved: {self.reserved.hex()}\n' \
|
||||
f'- Kernel cmdline: {self.cmdline!r}\n'
|
||||
|
||||
def _from_dict(self, data: dict[str, typing.Any]) -> None:
|
||||
type = data.get('type')
|
||||
header_version = data.get('header_version')
|
||||
|
||||
if type != 'android':
|
||||
raise WrongFormat(f'Unknown type: {type}')
|
||||
elif header_version not in (3, 4):
|
||||
raise WrongFormat(f'Unknown header version: {header_version}')
|
||||
|
||||
self.header_version = header_version
|
||||
self.os_version = data['os_version']
|
||||
self.reserved = data['reserved']
|
||||
self.cmdline = data['cmdline']
|
||||
|
||||
def to_dict(self) -> dict[str, typing.Any]:
|
||||
return {
|
||||
'type': 'android',
|
||||
'header_version': self.header_version,
|
||||
'os_version': self.os_version,
|
||||
'reserved': self.reserved,
|
||||
'cmdline': self.cmdline,
|
||||
}
|
||||
|
||||
|
||||
_RamdiskMeta = collections.namedtuple(
|
||||
'_RamdiskMeta', ['type', 'name', 'board_id'])
|
||||
|
||||
|
||||
class _VendorBootImageV3Through4(BootImage):
|
||||
def _from_file(self, f: typing.BinaryIO) -> None:
|
||||
# Common fields for both v3 and v4
|
||||
magic, header_version, page_size, kernel_addr, ramdisk_addr, \
|
||||
vendor_ramdisk_size, cmdline, tags_addr, name, header_size, \
|
||||
dtb_size, dtb_addr = VENDOR_BOOT_IMG_HDR_V3.unpack(
|
||||
util.read_exact(f, VENDOR_BOOT_IMG_HDR_V3.size))
|
||||
|
||||
if magic != VENDOR_BOOT_MAGIC:
|
||||
raise WrongFormat(f'Unknown magic: {magic}')
|
||||
elif header_version not in (3, 4):
|
||||
raise WrongFormat(f'Unknown header version: {header_version}')
|
||||
|
||||
# Parse v4 fields
|
||||
if header_version == 4:
|
||||
vendor_ramdisk_table_size, vendor_ramdisk_table_entry_num, \
|
||||
vendor_ramdisk_table_entry_size, bootconfig_size = \
|
||||
VENDOR_BOOT_IMG_HDR_V4_EXTRA.unpack(
|
||||
util.read_exact(f, VENDOR_BOOT_IMG_HDR_V4_EXTRA.size))
|
||||
|
||||
if vendor_ramdisk_table_entry_size != \
|
||||
VENDOR_RAMDISK_TABLE_ENTRY_V4.size:
|
||||
raise ValueError('Invalid ramdisk table entry size: '
|
||||
f'{vendor_ramdisk_table_entry_size}')
|
||||
elif vendor_ramdisk_table_size != vendor_ramdisk_table_entry_num \
|
||||
* vendor_ramdisk_table_entry_size:
|
||||
raise ValueError('Invalid ramdisk table size: '
|
||||
f'{vendor_ramdisk_table_size}')
|
||||
|
||||
if f.tell() != header_size:
|
||||
raise ValueError(f'Invalid header size: {header_size}')
|
||||
|
||||
self.page_size = page_size
|
||||
self.header_version = header_version
|
||||
self.kernel_addr = kernel_addr
|
||||
self.ramdisk_addr = ramdisk_addr
|
||||
self.cmdline = cmdline.rstrip(b'\0')
|
||||
self.tags_addr = tags_addr
|
||||
self.name = name.rstrip(b'\0')
|
||||
self.dtb_addr = dtb_addr
|
||||
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
vendor_ramdisk_offset = f.tell()
|
||||
|
||||
if header_version == 3:
|
||||
# v3 has one big ramdisk
|
||||
self.ramdisks.append(util.read_exact(f, vendor_ramdisk_size))
|
||||
else:
|
||||
# v4 has multiple ramdisks, processed later
|
||||
f.seek(vendor_ramdisk_size, os.SEEK_CUR)
|
||||
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if dtb_size > 0:
|
||||
self.dtb = util.read_exact(f, dtb_size)
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if header_version == 4:
|
||||
self.ramdisks_meta = []
|
||||
|
||||
total_ramdisk_size = 0
|
||||
|
||||
for _ in range(0, vendor_ramdisk_table_entry_num):
|
||||
ramdisk_size, ramdisk_offset, ramdisk_type, ramdisk_name, \
|
||||
board_id = VENDOR_RAMDISK_TABLE_ENTRY_V4.unpack(
|
||||
util.read_exact(f, VENDOR_RAMDISK_TABLE_ENTRY_V4.size))
|
||||
|
||||
table_offset = f.tell()
|
||||
f.seek(vendor_ramdisk_offset + ramdisk_offset)
|
||||
|
||||
self.ramdisks.append(util.read_exact(f, ramdisk_size))
|
||||
self.ramdisks_meta.append(_RamdiskMeta(
|
||||
ramdisk_type,
|
||||
ramdisk_name.rstrip(b'\0'),
|
||||
board_id,
|
||||
))
|
||||
|
||||
f.seek(table_offset)
|
||||
|
||||
total_ramdisk_size += ramdisk_size
|
||||
|
||||
if total_ramdisk_size != vendor_ramdisk_size:
|
||||
raise ValueError('Invalid vendor ramdisk size: '
|
||||
f'{vendor_ramdisk_size}')
|
||||
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
if bootconfig_size > 0:
|
||||
self.bootconfig = util.read_exact(f, bootconfig_size)
|
||||
padding.read_skip(f, page_size)
|
||||
|
||||
def generate(self, f: typing.BinaryIO) -> None:
|
||||
if self.header_version == 3:
|
||||
if len(self.ramdisks) > 1:
|
||||
raise ValueError('Only one ramdisk is supported')
|
||||
elif self.bootconfig is not None:
|
||||
raise ValueError('Boot config is not supported')
|
||||
else:
|
||||
if len(self.ramdisks) != len(self.ramdisks_meta):
|
||||
raise ValueError('Mismatched ramdisk and ramdisk_meta')
|
||||
|
||||
if self.second is not None:
|
||||
raise ValueError('Second stage bootloader is not supported')
|
||||
elif self.recovery_dtbo is not None:
|
||||
raise ValueError('Recovery dtbo/acpio is not supported')
|
||||
|
||||
vendor_ramdisk_size = sum(len(r) for r in self.ramdisks)
|
||||
|
||||
f.write(VENDOR_BOOT_IMG_HDR_V3.pack(
|
||||
VENDOR_BOOT_MAGIC,
|
||||
self.header_version,
|
||||
self.page_size,
|
||||
self.kernel_addr,
|
||||
self.ramdisk_addr,
|
||||
vendor_ramdisk_size,
|
||||
self.cmdline,
|
||||
self.tags_addr,
|
||||
self.name,
|
||||
VENDOR_BOOT_IMG_HDR_V3.size + (
|
||||
VENDOR_BOOT_IMG_HDR_V4_EXTRA.size
|
||||
if self.header_version == 4 else 0),
|
||||
len(self.dtb) if self.dtb else 0,
|
||||
self.dtb_addr,
|
||||
))
|
||||
|
||||
if self.header_version == 4:
|
||||
f.write(VENDOR_BOOT_IMG_HDR_V4_EXTRA.pack(
|
||||
len(self.ramdisks) * VENDOR_RAMDISK_TABLE_ENTRY_V4.size,
|
||||
len(self.ramdisks),
|
||||
VENDOR_RAMDISK_TABLE_ENTRY_V4.size,
|
||||
len(self.bootconfig) if self.bootconfig else 0,
|
||||
))
|
||||
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
for ramdisk in self.ramdisks:
|
||||
f.write(ramdisk)
|
||||
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.dtb:
|
||||
f.write(self.dtb)
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.header_version == 4:
|
||||
ramdisk_offset = 0
|
||||
|
||||
for ramdisk, meta in zip(self.ramdisks, self.ramdisks_meta):
|
||||
f.write(VENDOR_RAMDISK_TABLE_ENTRY_V4.pack(
|
||||
len(ramdisk),
|
||||
ramdisk_offset,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.board_id,
|
||||
))
|
||||
|
||||
ramdisk_offset += len(ramdisk)
|
||||
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
if self.bootconfig:
|
||||
f.write(self.bootconfig)
|
||||
padding.write(f, self.page_size)
|
||||
|
||||
def __str__(self) -> str:
|
||||
dtb_size = len(self.dtb) if self.dtb else 0
|
||||
|
||||
result = \
|
||||
f'Vendor boot image v{self.header_version} header:\n' \
|
||||
f'- Page size: {self.page_size}\n' \
|
||||
f'- Kernel address: 0x{self.kernel_addr:x}\n'
|
||||
|
||||
if self.header_version == 3:
|
||||
ramdisk_size = len(self.ramdisks[0]) if self.ramdisks else 0
|
||||
result += f'- Ramdisk size: {ramdisk_size}\n'
|
||||
|
||||
result += \
|
||||
f'- Ramdisk address: 0x{self.ramdisk_addr:x}\n' \
|
||||
f'- Kernel cmdline: {self.cmdline!r}\n' \
|
||||
f'- Kernel tags address: 0x{self.tags_addr:x}\n' \
|
||||
f'- Name: {self.name!r}\n' \
|
||||
f'- Device tree size: {dtb_size}\n' \
|
||||
f'- Device tree address: {self.dtb_addr}\n'
|
||||
|
||||
if self.header_version == 4:
|
||||
for ramdisk, meta in zip(self.ramdisks, self.ramdisks_meta):
|
||||
result += \
|
||||
'- Ramdisk:\n' \
|
||||
f' - Size: {len(ramdisk)}\n' \
|
||||
f' - Type: {meta.type}\n' \
|
||||
f' - Name: {meta.name}\n' \
|
||||
f' - Board ID: {meta.board_id.hex()}\n'
|
||||
|
||||
bootconfig_size = len(self.bootconfig) if self.bootconfig else 0
|
||||
|
||||
result += f'- Bootconfig size: {bootconfig_size}\n'
|
||||
|
||||
return result
|
||||
|
||||
def _from_dict(self, data: dict[str, typing.Any]) -> None:
|
||||
type = data.get('type')
|
||||
header_version = data.get('header_version')
|
||||
|
||||
if type != 'vendor':
|
||||
raise WrongFormat(f'Unknown type: {type}')
|
||||
elif header_version not in (3, 4):
|
||||
raise WrongFormat(f'Unknown header version: {header_version}')
|
||||
|
||||
self.header_version = header_version
|
||||
self.page_size = data['page_size']
|
||||
self.kernel_addr = data['kernel_address']
|
||||
self.ramdisk_addr = data['ramdisk_address']
|
||||
self.cmdline = data['cmdline']
|
||||
self.tags_addr = data['tags_address']
|
||||
self.name = data['name']
|
||||
self.dtb_addr = data['dtb_address']
|
||||
|
||||
if header_version == 4:
|
||||
self.ramdisks_meta = []
|
||||
for meta in data['ramdisk_meta']:
|
||||
self.ramdisks_meta.append(_RamdiskMeta(
|
||||
meta['type'],
|
||||
meta['name'],
|
||||
meta['board_id'],
|
||||
))
|
||||
|
||||
def to_dict(self) -> dict[str, typing.Any]:
|
||||
result = {
|
||||
'type': 'vendor',
|
||||
'header_version': self.header_version,
|
||||
'page_size': self.page_size,
|
||||
'kernel_address': self.kernel_addr,
|
||||
'ramdisk_address': self.ramdisk_addr,
|
||||
'cmdline': self.cmdline,
|
||||
'tags_address': self.tags_addr,
|
||||
'name': self.name,
|
||||
'dtb_address': self.dtb_addr,
|
||||
}
|
||||
|
||||
if self.header_version == 4:
|
||||
result['ramdisk_meta'] = []
|
||||
for meta in self.ramdisks_meta:
|
||||
result['ramdisk_meta'].append({
|
||||
'type': meta.type,
|
||||
'name': meta.name,
|
||||
'board_id': meta.board_id,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def load_autodetect(f: typing.BinaryIO) -> BootImage:
|
||||
for cls in (
|
||||
_BootImageV0Through2,
|
||||
_BootImageV3Through4,
|
||||
_VendorBootImageV3Through4,
|
||||
):
|
||||
try:
|
||||
f.seek(0)
|
||||
return cls(f=f)
|
||||
except WrongFormat:
|
||||
continue
|
||||
|
||||
raise ValueError('Unknown boot image format')
|
||||
|
||||
|
||||
def create_from_dict(data: dict) -> BootImage:
|
||||
for cls in (
|
||||
_BootImageV0Through2,
|
||||
_BootImageV3Through4,
|
||||
_VendorBootImageV3Through4,
|
||||
):
|
||||
try:
|
||||
return cls(data=data)
|
||||
except WrongFormat:
|
||||
continue
|
||||
|
||||
raise ValueError('Unknown boot image format')
|
||||
@@ -1,187 +0,0 @@
|
||||
import enum
|
||||
import gzip
|
||||
import typing
|
||||
|
||||
import lz4.block
|
||||
|
||||
from .. import util
|
||||
|
||||
|
||||
GZIP_MAGIC = b'\x1f\x8b'
|
||||
|
||||
|
||||
class Lz4Legacy:
|
||||
MAGIC = b'\x02\x21\x4c\x18'
|
||||
MAX_BLOCK_SIZE = 8 * 1024 * 1024
|
||||
|
||||
def __init__(self, fp: typing.BinaryIO,
|
||||
mode: typing.Literal['rb', 'wb'] = 'rb'):
|
||||
if mode not in ('rb', 'wb'):
|
||||
raise ValueError(f'Invalid mode: {mode}')
|
||||
|
||||
self.fp = fp
|
||||
self.mode = mode
|
||||
|
||||
if mode == 'rb':
|
||||
magic = util.read_exact(self.fp, len(self.MAGIC))
|
||||
if magic != self.MAGIC:
|
||||
raise ValueError(f'Invalid magic: {magic!r}')
|
||||
|
||||
self.rblock = b''
|
||||
self.rblock_offset = 0
|
||||
else:
|
||||
self.fp.write(self.MAGIC)
|
||||
|
||||
self.wblock = bytearray()
|
||||
|
||||
self.file_offset = 0
|
||||
|
||||
def __enter__(self) -> 'Lz4Legacy':
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_args) -> None:
|
||||
self.close()
|
||||
|
||||
def _read_block(self) -> None:
|
||||
if self.rblock_offset < len(self.rblock):
|
||||
# Haven't finished reading block yet
|
||||
return
|
||||
|
||||
size_raw = self.fp.read(4)
|
||||
if not size_raw or size_raw == self.MAGIC:
|
||||
self.rblock = b''
|
||||
self.rblock_offset = 0
|
||||
return
|
||||
elif len(size_raw) != 4:
|
||||
raise EOFError('Failed to read block size')
|
||||
|
||||
size_compressed = int.from_bytes(size_raw, 'little')
|
||||
|
||||
compressed = util.read_exact(self.fp, size_compressed)
|
||||
self.rblock = lz4.block.decompress(compressed, self.MAX_BLOCK_SIZE)
|
||||
self.rblock_offset = 0
|
||||
|
||||
def _write_block(self, force=False) -> None:
|
||||
if not force and len(self.wblock) < self.MAX_BLOCK_SIZE:
|
||||
# Block not fully filled yet
|
||||
return
|
||||
|
||||
compressed = lz4.block.compress(
|
||||
self.wblock,
|
||||
mode='high_compression',
|
||||
compression=12,
|
||||
store_size=False,
|
||||
)
|
||||
|
||||
self.fp.write(len(compressed).to_bytes(4, 'little'))
|
||||
self.fp.write(compressed)
|
||||
|
||||
self.wblock.clear()
|
||||
|
||||
def read(self, size=None) -> bytes:
|
||||
assert self.mode == 'rb'
|
||||
|
||||
result = bytearray()
|
||||
|
||||
while size is None or size > 0:
|
||||
self._read_block()
|
||||
|
||||
to_read = len(self.rblock) - self.rblock_offset
|
||||
if to_read == 0:
|
||||
# EOF
|
||||
break
|
||||
elif size is not None:
|
||||
to_read = min(to_read, size)
|
||||
|
||||
result.extend(self.rblock[self.rblock_offset:
|
||||
self.rblock_offset + to_read])
|
||||
|
||||
self.rblock_offset += to_read
|
||||
self.file_offset += to_read
|
||||
|
||||
if size is not None:
|
||||
size -= to_read
|
||||
|
||||
return result
|
||||
|
||||
def write(self, data: bytes) -> int:
|
||||
assert self.mode == 'wb'
|
||||
|
||||
offset = 0
|
||||
|
||||
while offset < len(data):
|
||||
self._write_block()
|
||||
|
||||
to_write = min(
|
||||
self.MAX_BLOCK_SIZE - len(self.wblock),
|
||||
len(data) - offset,
|
||||
)
|
||||
|
||||
self.wblock.extend(data[offset:offset + to_write])
|
||||
|
||||
self.file_offset += to_write
|
||||
offset += to_write
|
||||
|
||||
return len(data)
|
||||
|
||||
def flush(self) -> None:
|
||||
assert self.mode == 'wb'
|
||||
|
||||
self._write_block(force=True)
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
if self.mode == 'wb':
|
||||
self.flush()
|
||||
finally:
|
||||
self.mode = 'closed'
|
||||
|
||||
def tell(self) -> int:
|
||||
return self.file_offset
|
||||
|
||||
|
||||
Format = enum.Enum('Format', ['GZIP', 'LZ4_LEGACY'])
|
||||
|
||||
|
||||
_MAGIC_TO_FORMAT = {
|
||||
GZIP_MAGIC: Format.GZIP,
|
||||
Lz4Legacy.MAGIC: Format.LZ4_LEGACY,
|
||||
}
|
||||
_MAGIC_MAX_SIZE = max(len(m) for m in _MAGIC_TO_FORMAT)
|
||||
|
||||
|
||||
class CompressedFile:
|
||||
def __init__(
|
||||
self,
|
||||
fp: typing.BinaryIO,
|
||||
mode: typing.Literal['rb', 'wb'] = 'rb',
|
||||
format: typing.Optional[Format] = None,
|
||||
raw_if_unknown = False,
|
||||
):
|
||||
if mode == 'rb' and not format:
|
||||
magic = fp.read(_MAGIC_MAX_SIZE)
|
||||
fp.seek(0)
|
||||
|
||||
for m, f in _MAGIC_TO_FORMAT.items():
|
||||
if magic.startswith(m):
|
||||
format = f
|
||||
break
|
||||
|
||||
if format == Format.GZIP:
|
||||
format_fp = gzip.GzipFile(fileobj=fp, mode=mode, mtime=0)
|
||||
elif format == Format.LZ4_LEGACY:
|
||||
format_fp = Lz4Legacy(fp, mode)
|
||||
elif raw_if_unknown:
|
||||
format_fp = fp
|
||||
else:
|
||||
raise ValueError('Unknown compression format')
|
||||
|
||||
self.fp = format_fp
|
||||
self.format = format
|
||||
|
||||
def __enter__(self):
|
||||
self.fp.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_args):
|
||||
self.fp.__exit__(*exc_args)
|
||||
@@ -1,282 +0,0 @@
|
||||
# This is a miniature implementation of cpio, originally written for
|
||||
# DualBootPatcher, supporting only enough of the file format for messing with
|
||||
# boot image ramdisks. Only the "new format" for cpio entries are supported.
|
||||
|
||||
import stat
|
||||
import typing
|
||||
|
||||
from . import padding
|
||||
from .. import util
|
||||
|
||||
MAGIC_NEW = b'070701' # new format
|
||||
MAGIC_NEW_CRC = b'070702' # new format w/crc
|
||||
|
||||
# Constants from cpio.h
|
||||
|
||||
# A header with a filename "TRAILER!!!" indicates the end of the archive.
|
||||
CPIO_TRAILER = b'TRAILER!!!'
|
||||
|
||||
C_ISCTG = 0o0110000
|
||||
|
||||
IO_BLOCK_SIZE = 512
|
||||
|
||||
|
||||
def _read_int(f: typing.BinaryIO) -> int:
|
||||
return int(util.read_exact(f, 8), 16)
|
||||
|
||||
|
||||
def _write_int(f: typing.BinaryIO, value: int) -> int:
|
||||
if value < 0 or value > 0xffffffff:
|
||||
raise ValueError(f'{value} out of range for 32-bit integer')
|
||||
|
||||
return f.write(b'%08x' % value)
|
||||
|
||||
|
||||
class CpioEntryNew:
|
||||
# c_magic - "070701" for "new" portable format
|
||||
# "070702" for CRC format
|
||||
# c_ino
|
||||
# c_mode
|
||||
# c_uid
|
||||
# c_gid
|
||||
# c_nlink
|
||||
# c_mtime
|
||||
# c_filesize - must be 0 for FIFOs and directories
|
||||
# c_dev_maj
|
||||
# c_dev_min
|
||||
# c_rdev_maj - only valid for chr and blk special files
|
||||
# c_rdev_min - only valid for chr and blk special files
|
||||
# c_namesize - count includes terminating NUL in pathname
|
||||
# c_chksum - 0 for "new" portable format; for CRC format
|
||||
# the sum of all the bytes in the file
|
||||
|
||||
@staticmethod
|
||||
def new_trailer() -> 'CpioEntryNew':
|
||||
entry = CpioEntryNew()
|
||||
entry.nlink = 1 # Must be 1 for crc format
|
||||
entry.name = CPIO_TRAILER
|
||||
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def new_symlink(link_target: bytes, name: bytes) -> 'CpioEntryNew':
|
||||
if not link_target:
|
||||
raise ValueError('Symlink target is empty')
|
||||
elif not name:
|
||||
raise ValueError('Symlink name is empty')
|
||||
|
||||
entry = CpioEntryNew()
|
||||
entry.mode = stat.S_IFLNK | 0o777
|
||||
entry.nlink = 1
|
||||
entry.name = name
|
||||
entry.content = link_target
|
||||
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def new_directory(name: bytes, perms: int = 0o755) -> 'CpioEntryNew':
|
||||
if not name:
|
||||
raise ValueError('Directory name is empty')
|
||||
|
||||
entry = CpioEntryNew()
|
||||
entry.mode = stat.S_IFDIR | stat.S_IMODE(perms)
|
||||
entry.nlink = 1
|
||||
entry.name = name
|
||||
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def new_file(name: bytes, perms: int = 0o644,
|
||||
data: bytes = b'') -> 'CpioEntryNew':
|
||||
if not name:
|
||||
raise ValueError('File name is empty')
|
||||
|
||||
entry = CpioEntryNew()
|
||||
entry.mode = stat.S_IFREG | stat.S_IMODE(perms)
|
||||
entry.nlink = 1
|
||||
entry.name = name
|
||||
entry.content = data
|
||||
|
||||
return entry
|
||||
|
||||
def __init__(self, f: typing.Optional[typing.BinaryIO] = None) -> None:
|
||||
super(CpioEntryNew, self).__init__()
|
||||
|
||||
if f is None:
|
||||
self.magic = MAGIC_NEW
|
||||
self.ino = 0
|
||||
self.mode = 0
|
||||
self.uid = 0
|
||||
self.gid = 0
|
||||
self.nlink = 0
|
||||
self.mtime = 0
|
||||
self.filesize = 0
|
||||
self.dev_maj = 0
|
||||
self.dev_min = 0
|
||||
self.rdev_maj = 0
|
||||
self.rdev_min = 0
|
||||
self.namesize = 0
|
||||
self.chksum = 0
|
||||
|
||||
self._name = b''
|
||||
self._content = b''
|
||||
else:
|
||||
self.magic = util.read_exact(f, 6)
|
||||
if self.magic != MAGIC_NEW and self.magic != MAGIC_NEW_CRC:
|
||||
raise Exception(f'Unknown magic: {self.magic!r}')
|
||||
|
||||
self.ino = _read_int(f)
|
||||
self.mode = _read_int(f)
|
||||
self.uid = _read_int(f)
|
||||
self.gid = _read_int(f)
|
||||
self.nlink = _read_int(f)
|
||||
self.mtime = _read_int(f)
|
||||
self.filesize = _read_int(f)
|
||||
self.dev_maj = _read_int(f)
|
||||
self.dev_min = _read_int(f)
|
||||
self.rdev_maj = _read_int(f)
|
||||
self.rdev_min = _read_int(f)
|
||||
self.namesize = _read_int(f)
|
||||
self.chksum = _read_int(f)
|
||||
|
||||
# Filename
|
||||
self._name = util.read_exact(f, self.namesize - 1)
|
||||
# Discard NULL terminator
|
||||
util.read_exact(f, 1)
|
||||
padding.read_skip(f, 4)
|
||||
|
||||
# File contents
|
||||
self._content = util.read_exact(f, self.filesize)
|
||||
padding.read_skip(f, 4)
|
||||
|
||||
def write(self, f: typing.BinaryIO):
|
||||
if len(self.magic) != 6:
|
||||
raise ValueError(f'Magic is not 6 bytes: {self.magic!r}')
|
||||
|
||||
f.write(self.magic)
|
||||
|
||||
_write_int(f, self.ino)
|
||||
_write_int(f, self.mode)
|
||||
_write_int(f, self.uid)
|
||||
_write_int(f, self.gid)
|
||||
_write_int(f, self.nlink)
|
||||
_write_int(f, self.mtime)
|
||||
_write_int(f, self.filesize)
|
||||
_write_int(f, self.dev_maj)
|
||||
_write_int(f, self.dev_min)
|
||||
_write_int(f, self.rdev_maj)
|
||||
_write_int(f, self.rdev_min)
|
||||
_write_int(f, self.namesize)
|
||||
_write_int(f, self.chksum)
|
||||
|
||||
# Filename
|
||||
f.write(self._name)
|
||||
f.write(b'\x00')
|
||||
padding.write(f, 4)
|
||||
|
||||
# File contents
|
||||
f.write(self._content)
|
||||
padding.write(f, 4)
|
||||
|
||||
@property
|
||||
def name(self) -> bytes:
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value: bytes):
|
||||
self._name = value
|
||||
self.namesize = len(value) + 1
|
||||
|
||||
@property
|
||||
def content(self) -> bytes:
|
||||
return self._content
|
||||
|
||||
@content.setter
|
||||
def content(self, value: bytes):
|
||||
self._content = value
|
||||
self.filesize = len(value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
filetype = stat.S_IFMT(self.mode)
|
||||
|
||||
if stat.S_ISDIR(self.mode):
|
||||
ftypestr = 'directory'
|
||||
elif stat.S_ISLNK(self.mode):
|
||||
ftypestr = 'symbolic link'
|
||||
elif stat.S_ISREG(self.mode):
|
||||
ftypestr = 'regular file'
|
||||
elif stat.S_ISFIFO(self.mode):
|
||||
ftypestr = 'pipe'
|
||||
elif stat.S_ISCHR(self.mode):
|
||||
ftypestr = 'character device'
|
||||
elif stat.S_ISBLK(self.mode):
|
||||
ftypestr = 'block device'
|
||||
elif stat.S_ISSOCK(self.mode):
|
||||
ftypestr = 'socket'
|
||||
elif filetype == C_ISCTG:
|
||||
ftypestr = 'reserved'
|
||||
else:
|
||||
ftypestr = 'unknown (%o)' % filetype
|
||||
|
||||
return \
|
||||
f'Filename: {self.name!r}\n' \
|
||||
f'Filetype: {ftypestr}\n' \
|
||||
f'Magic: {self.magic!r}\n' \
|
||||
f'Inode: {self.ino}\n' \
|
||||
f'Mode: {self.mode:o}\n' \
|
||||
f'Permissions: {self.mode - filetype:o}\n' \
|
||||
f'UID: {self.uid}\n' \
|
||||
f'GID: {self.gid}\n' \
|
||||
f'Links: {self.nlink}\n' \
|
||||
f'Modified: {self.mtime}\n' \
|
||||
f'File size: {self.filesize}\n' \
|
||||
f'Device: {self.dev_maj:x},{self.dev_min:x}\n' \
|
||||
f'Device ID: {self.rdev_maj:x},{self.rdev_min:x}\n' \
|
||||
f'Filename length: {self.namesize}\n' \
|
||||
f'Checksum: {self.chksum:x}\n'
|
||||
|
||||
|
||||
def load(f: typing.BinaryIO, include_trailer: bool = False,
|
||||
reassign_inodes: bool = True) -> list[CpioEntryNew]:
|
||||
entries = []
|
||||
|
||||
while True:
|
||||
entry = CpioEntryNew(f)
|
||||
|
||||
if stat.S_IFMT(entry.mode) != stat.S_IFDIR and entry.nlink > 1:
|
||||
raise ValueError(f'Hard links are not supported: {entry.name!r}')
|
||||
|
||||
# Inodes are reassigned on save
|
||||
if reassign_inodes:
|
||||
entry.ino = 0
|
||||
|
||||
if entry.name == CPIO_TRAILER:
|
||||
if include_trailer:
|
||||
entries.append(entry)
|
||||
break
|
||||
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def save(f: typing.BinaryIO, entries: list[CpioEntryNew], sort=True,
|
||||
pad_to_block_size=False):
|
||||
inode = 300000
|
||||
|
||||
if sort:
|
||||
entries = sorted(entries, key=lambda e: e.name)
|
||||
|
||||
for entry in entries:
|
||||
entry.ino = inode
|
||||
inode += 1
|
||||
|
||||
entry.write(f)
|
||||
|
||||
trailer = CpioEntryNew.new_trailer()
|
||||
trailer.ino = inode
|
||||
trailer.write(f)
|
||||
|
||||
# Pad until end of block
|
||||
if pad_to_block_size:
|
||||
padding.write(f, IO_BLOCK_SIZE)
|
||||
@@ -1,47 +0,0 @@
|
||||
import os
|
||||
import typing
|
||||
|
||||
|
||||
def _is_power_of_2(n: int) -> bool:
|
||||
if hasattr(n, 'bit_count'):
|
||||
return n.bit_count() == 1
|
||||
else:
|
||||
return bin(n).count('1') == 1
|
||||
|
||||
|
||||
def calc(offset: int, page_size: int) -> int:
|
||||
'''
|
||||
Calculate the amount of padding that needs to be added to align the
|
||||
specified offset to a page boundary. The page size must be a power of 2.
|
||||
'''
|
||||
|
||||
if not _is_power_of_2(page_size):
|
||||
raise ValueError(f'{page_size} is not a power of 2')
|
||||
|
||||
return (page_size - (offset & (page_size - 1))) & (page_size - 1)
|
||||
|
||||
|
||||
def read_skip(f: typing.BinaryIO, page_size: int) -> int:
|
||||
'''
|
||||
Seek file to the next page boundary if it is not already at a page
|
||||
boundary. If the file does not support seeking, then data is read and
|
||||
discarded.
|
||||
'''
|
||||
|
||||
padding = calc(f.tell(), page_size)
|
||||
|
||||
if hasattr(f, 'seek'):
|
||||
f.seek(padding, os.SEEK_CUR)
|
||||
else:
|
||||
f.read(padding)
|
||||
|
||||
return padding
|
||||
|
||||
|
||||
def write(f: typing.BinaryIO, page_size: int) -> int:
|
||||
'''
|
||||
Write null bytes to pad the file to the next page boundary if it is not
|
||||
already at a page boundary.
|
||||
'''
|
||||
|
||||
return f.write(calc(f.tell(), page_size) * b'\x00')
|
||||
-731
@@ -1,731 +0,0 @@
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
import copy
|
||||
import dataclasses
|
||||
import graphlib
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
import time
|
||||
import typing
|
||||
import unittest.mock
|
||||
import zipfile
|
||||
|
||||
import avbtool
|
||||
|
||||
from . import boot
|
||||
from . import openssl
|
||||
from . import ota
|
||||
from . import util
|
||||
from . import vbmeta
|
||||
from .formats import bootimage
|
||||
from .formats import compression
|
||||
from .formats import cpio
|
||||
|
||||
|
||||
PATH_METADATA = 'META-INF/com/android/metadata'
|
||||
PATH_METADATA_PB = f'{PATH_METADATA}.pb'
|
||||
PATH_OTACERT = 'META-INF/com/android/otacert'
|
||||
PATH_PAYLOAD = 'payload.bin'
|
||||
PATH_PROPERTIES = 'payload_properties.txt'
|
||||
|
||||
PARTITION_PRIORITIES = {
|
||||
# The kernel is always in boot
|
||||
'@gki_kernel': ('boot',),
|
||||
# Devices launching with Android 13 use a GKI init_boot ramdisk
|
||||
'@gki_ramdisk': ('init_boot', 'boot'),
|
||||
# OnePlus devices have a recovery image
|
||||
'@otacerts': ('recovery', 'vendor_boot', 'boot'),
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PatchContext:
|
||||
replace_images: dict[str, os.PathLike[str]]
|
||||
boot_partition: str
|
||||
root_patch: typing.Optional[boot.BootImagePatch]
|
||||
clear_vbmeta_flags: bool
|
||||
privkey_avb: os.PathLike[str]
|
||||
passphrase_avb: str
|
||||
privkey_ota: os.PathLike[str]
|
||||
passphrase_ota: str
|
||||
cert_ota: os.PathLike[str]
|
||||
|
||||
|
||||
def print_status(*args, **kwargs):
|
||||
print('\x1b[1m*****', *args, '*****\x1b[0m', **kwargs)
|
||||
|
||||
|
||||
def print_warning(*args, **kwargs):
|
||||
print('\x1b[1;31m*****', '[WARNING]', *args, '*****\x1b[0m', **kwargs)
|
||||
|
||||
|
||||
def get_partitions_by_type(manifest):
|
||||
all_partitions = set(p.partition_name for p in manifest.partitions)
|
||||
by_type = {}
|
||||
|
||||
for t, candidates in PARTITION_PRIORITIES.items():
|
||||
partition = next((p for p in candidates if p in all_partitions), None)
|
||||
if partition is None:
|
||||
raise ValueError(f'Cannot find partition of type: {t}')
|
||||
|
||||
by_type[t] = partition
|
||||
|
||||
for partition in all_partitions:
|
||||
if 'vbmeta' in partition:
|
||||
by_type[f'@vbmeta:{partition}'] = partition
|
||||
|
||||
return by_type
|
||||
|
||||
|
||||
def get_required_images(manifest, boot_partition, with_root):
|
||||
all_partitions = set(p.partition_name for p in manifest.partitions)
|
||||
by_type = get_partitions_by_type(manifest)
|
||||
images = {k: v for k, v in by_type.items()
|
||||
if k == '@otacerts' or k.startswith('@vbmeta:')}
|
||||
|
||||
if with_root:
|
||||
if boot_partition in by_type:
|
||||
images['@rootpatch'] = by_type[boot_partition]
|
||||
elif boot_partition in all_partitions:
|
||||
images['@rootpatch'] = boot_partition
|
||||
else:
|
||||
raise ValueError(f'Boot partition not found: {boot_partition}')
|
||||
|
||||
return images
|
||||
|
||||
|
||||
def get_vbmeta_patch_order(avb, image_paths, vbmeta_images):
|
||||
dep_graph = vbmeta.get_vbmeta_deps(
|
||||
avb, {n: image_paths[n] for n in vbmeta_images})
|
||||
|
||||
# Only keep dependencies among the subset of images we're working with
|
||||
dep_graph = {n: {d for d in deps if d in image_paths}
|
||||
for n, deps in dep_graph.items() if n in image_paths}
|
||||
|
||||
# Avoid patching vbmeta images that don't need changes
|
||||
while True:
|
||||
unneeded_vbmeta = set(n for n, d in dep_graph.items()
|
||||
if n in vbmeta_images and not d)
|
||||
if not unneeded_vbmeta:
|
||||
break
|
||||
|
||||
dep_graph = {n: {d for d in deps if d not in unneeded_vbmeta}
|
||||
for n, deps in dep_graph.items()
|
||||
if n not in unneeded_vbmeta}
|
||||
|
||||
full_order = graphlib.TopologicalSorter(dep_graph).static_order()
|
||||
order = [n for n in full_order if n in vbmeta_images]
|
||||
|
||||
return dep_graph, order
|
||||
|
||||
|
||||
def patch_ota_payload(f_in, open_more_f_in, f_out, file_size,
|
||||
context: PatchContext):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
extract_dir = os.path.join(temp_dir, 'extract')
|
||||
patch_dir = os.path.join(temp_dir, 'patch')
|
||||
payload_dir = os.path.join(temp_dir, 'payload')
|
||||
os.mkdir(extract_dir)
|
||||
os.mkdir(patch_dir)
|
||||
os.mkdir(payload_dir)
|
||||
|
||||
version, manifest, blob_offset = ota.parse_payload(f_in)
|
||||
all_partitions = set(p.partition_name for p in manifest.partitions)
|
||||
image_paths = {}
|
||||
|
||||
# Use user-provided partition images if provided. This may be a larger
|
||||
# set than what's needed for our patches.
|
||||
for name, path in context.replace_images.items():
|
||||
if name not in all_partitions:
|
||||
raise ValueError(
|
||||
f'Cannot replace non-existent partition: {name}')
|
||||
|
||||
image_paths[name] = path
|
||||
|
||||
# Extract remaining required partition images from the original payload.
|
||||
required_images = get_required_images(manifest, context.boot_partition,
|
||||
context.root_patch is not None)
|
||||
vbmeta_images = set(p for n, p in required_images.items()
|
||||
if n.startswith('@vbmeta:'))
|
||||
|
||||
to_extract = required_images.values() - image_paths.keys()
|
||||
for name in to_extract:
|
||||
image_paths[name] = os.path.join(extract_dir, f'{name}.img')
|
||||
|
||||
if to_extract:
|
||||
print_status('Extracting', ', '.join(sorted(to_extract)),
|
||||
'from the payload')
|
||||
ota.extract_images(open_more_f_in, manifest, blob_offset,
|
||||
extract_dir, to_extract)
|
||||
|
||||
image_patches = {}
|
||||
if context.root_patch is not None:
|
||||
image_patches.setdefault(required_images['@rootpatch'], []).append(
|
||||
context.root_patch)
|
||||
image_patches.setdefault(required_images['@otacerts'], []).append(
|
||||
boot.OtaCertPatch(context.cert_ota))
|
||||
|
||||
avb = avbtool.Avb()
|
||||
|
||||
print_status('Patching', ', '.join(sorted(image_patches)))
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=len(image_patches)) as executor:
|
||||
def apply_patches(image, patches):
|
||||
patched_path = os.path.join(patch_dir, f'{image}.img')
|
||||
|
||||
boot.patch_boot(
|
||||
avb,
|
||||
image_paths[image],
|
||||
patched_path,
|
||||
context.privkey_avb,
|
||||
context.passphrase_avb,
|
||||
True,
|
||||
patches,
|
||||
)
|
||||
|
||||
image_paths[image] = patched_path
|
||||
|
||||
futures = [executor.submit(apply_patches, i, p)
|
||||
for i, p in image_patches.items()]
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
future.result()
|
||||
|
||||
vbmeta_deps, vbmeta_order = \
|
||||
get_vbmeta_patch_order(avb, image_paths, vbmeta_images)
|
||||
print_status('Building', ', '.join(vbmeta_order))
|
||||
|
||||
for image in vbmeta_order:
|
||||
patched_path = os.path.join(patch_dir, f'{image}.img')
|
||||
|
||||
vbmeta.patch_vbmeta_image(
|
||||
avb,
|
||||
{n: p for n, p in image_paths.items()
|
||||
if n in vbmeta_deps[image]},
|
||||
image_paths[image],
|
||||
patched_path,
|
||||
context.privkey_avb,
|
||||
context.passphrase_avb,
|
||||
manifest.block_size,
|
||||
context.clear_vbmeta_flags,
|
||||
)
|
||||
|
||||
image_paths[image] = patched_path
|
||||
|
||||
# Don't replace untouched vbmeta images
|
||||
for image in vbmeta_images - set(vbmeta_order):
|
||||
del image_paths[image]
|
||||
|
||||
print_status('Updating OTA payload to reference new',
|
||||
', '.join(sorted(image_paths)))
|
||||
return ota.patch_payload(
|
||||
f_in,
|
||||
f_out,
|
||||
version,
|
||||
manifest,
|
||||
blob_offset,
|
||||
payload_dir,
|
||||
image_paths,
|
||||
file_size,
|
||||
context.privkey_ota,
|
||||
context.passphrase_ota,
|
||||
)
|
||||
|
||||
|
||||
def strip_bad_extra_fields(extra):
|
||||
offset = 0
|
||||
new_extra = bytearray()
|
||||
|
||||
while offset < len(extra):
|
||||
record_sig, record_len = \
|
||||
struct.unpack('<HH', extra[offset:offset + 4])
|
||||
|
||||
next_offset = offset + 4 + record_len
|
||||
|
||||
# 0xd935: ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID
|
||||
# 0x0001: zip64 size (zipfile will write a new record)
|
||||
if record_sig not in (0x0001, 0xd935):
|
||||
new_extra.extend(extra[offset:next_offset])
|
||||
|
||||
offset = next_offset
|
||||
|
||||
return new_extra
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def fix_streaming_local_header_sizes():
|
||||
'''
|
||||
Older Python versions don't set the local header's two 32-bit size fields to
|
||||
0xffffffff when writing a zip64 entry to an unseekable file. This function
|
||||
monkey patches zipfile's local file header serialization to manually fix
|
||||
this issue.
|
||||
'''
|
||||
|
||||
orig = zipfile.ZipInfo.FileHeader
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
blob = orig(*args, **kwargs)
|
||||
zip64 = kwargs.get('zip64')
|
||||
if zip64 is None:
|
||||
zip64 = args[0].file_size > zipfile.ZIP64_LIMIT or \
|
||||
args[0].compress_size > zipfile.ZIP64_LIMIT
|
||||
|
||||
fields = list(struct.unpack_from(zipfile.structFileHeader, blob))
|
||||
if fields[3] & (1 << 3) and zip64:
|
||||
fields[8] = 0xffffffff
|
||||
fields[9] = 0xffffffff
|
||||
|
||||
return struct.pack(zipfile.structFileHeader, *fields) + \
|
||||
blob[zipfile.sizeFileHeader:]
|
||||
else:
|
||||
return blob
|
||||
|
||||
with unittest.mock.patch('zipfile.ZipInfo.FileHeader', wrapper):
|
||||
yield
|
||||
|
||||
|
||||
def patch_ota_zip(f_zip_in, f_zip_out, context: PatchContext):
|
||||
with (
|
||||
zipfile.ZipFile(f_zip_in, 'r') as z_in,
|
||||
zipfile.ZipFile(f_zip_out, 'w') as z_out,
|
||||
):
|
||||
infolist = z_in.infolist()
|
||||
missing = {
|
||||
PATH_METADATA,
|
||||
PATH_METADATA_PB,
|
||||
PATH_OTACERT,
|
||||
PATH_PAYLOAD,
|
||||
PATH_PROPERTIES,
|
||||
}
|
||||
i_payload = -1
|
||||
i_properties = -1
|
||||
|
||||
for i, info in enumerate(infolist):
|
||||
if info.filename in missing:
|
||||
missing.remove(info.filename)
|
||||
|
||||
if info.filename == PATH_PAYLOAD:
|
||||
i_payload = i
|
||||
elif info.filename == PATH_PROPERTIES:
|
||||
i_properties = i
|
||||
|
||||
if not missing and i_payload >= 0 and i_properties >= 0:
|
||||
break
|
||||
|
||||
if missing:
|
||||
raise Exception(f'Missing files in zip: {missing}')
|
||||
|
||||
# Ensure payload is processed before properties
|
||||
if i_payload > i_properties:
|
||||
infolist[i_payload], infolist[i_properties] = \
|
||||
infolist[i_properties], infolist[i_payload]
|
||||
|
||||
properties = None
|
||||
metadata_info = None
|
||||
metadata_pb_info = None
|
||||
metadata_pb_raw = None
|
||||
|
||||
for info in infolist:
|
||||
out_info = copy.copy(info)
|
||||
out_info.extra = strip_bad_extra_fields(out_info.extra)
|
||||
|
||||
# Ignore because the plain-text legacy metadata file is regenerated
|
||||
# from the new metadata
|
||||
if info.filename == PATH_METADATA:
|
||||
metadata_info = out_info
|
||||
continue
|
||||
|
||||
# The existing metadata is needed to generate a new signed zip
|
||||
elif info.filename == PATH_METADATA_PB:
|
||||
metadata_pb_info = out_info
|
||||
|
||||
with z_in.open(info, 'r') as f_in:
|
||||
metadata_pb_raw = f_in.read()
|
||||
|
||||
continue
|
||||
|
||||
# Use the user's OTA certificate
|
||||
elif info.filename == PATH_OTACERT:
|
||||
print_status('Replacing', info.filename)
|
||||
|
||||
with (
|
||||
open(context.cert_ota, 'rb') as f_cert,
|
||||
z_out.open(out_info, 'w') as f_out,
|
||||
):
|
||||
shutil.copyfileobj(f_cert, f_out)
|
||||
|
||||
continue
|
||||
|
||||
# Copy other files, patching if needed
|
||||
with (
|
||||
z_in.open(info, 'r') as f_in,
|
||||
z_out.open(out_info, 'w') as f_out,
|
||||
):
|
||||
if info.filename == PATH_PAYLOAD:
|
||||
print_status('Patching', info.filename)
|
||||
|
||||
if info.compress_type != zipfile.ZIP_STORED:
|
||||
raise Exception(
|
||||
f'{info.filename} is not stored uncompressed')
|
||||
|
||||
properties = patch_ota_payload(
|
||||
f_in,
|
||||
lambda: z_in.open(info, 'r'),
|
||||
f_out,
|
||||
info.file_size,
|
||||
context,
|
||||
)
|
||||
|
||||
elif info.filename == PATH_PROPERTIES:
|
||||
print_status('Patching', info.filename)
|
||||
|
||||
if info.compress_type != zipfile.ZIP_STORED:
|
||||
raise Exception(
|
||||
f'{info.filename} is not stored uncompressed')
|
||||
|
||||
f_out.write(properties)
|
||||
|
||||
else:
|
||||
print_status('Copying', info.filename)
|
||||
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
print_status('Generating', PATH_METADATA, 'and', PATH_METADATA_PB)
|
||||
metadata = ota.add_metadata(
|
||||
z_out,
|
||||
metadata_info,
|
||||
metadata_pb_info,
|
||||
metadata_pb_raw,
|
||||
)
|
||||
|
||||
# Signing process needs to capture the zip central directory
|
||||
f_zip_out.start_capture()
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def patch_subcommand(args):
|
||||
output = args.output
|
||||
if output is None:
|
||||
output = args.input + '.patched'
|
||||
|
||||
if args.rootless:
|
||||
root_patch = None
|
||||
elif args.magisk is not None:
|
||||
root_patch = boot.MagiskRootPatch(
|
||||
args.magisk, args.magisk_preinit_device, args.magisk_random_seed)
|
||||
|
||||
try:
|
||||
root_patch.validate()
|
||||
except ValueError as e:
|
||||
if args.ignore_magisk_warnings:
|
||||
print_warning(e)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
root_patch = boot.PrepatchedImage(
|
||||
args.prepatched,
|
||||
args.ignore_prepatched_compat + 1,
|
||||
print_warning,
|
||||
)
|
||||
|
||||
# Get passphrases for keys
|
||||
passphrase_avb = openssl.prompt_passphrase(
|
||||
args.privkey_avb,
|
||||
args.passphrase_avb_env_var,
|
||||
args.passphrase_avb_file,
|
||||
)
|
||||
passphrase_ota = openssl.prompt_passphrase(
|
||||
args.privkey_ota,
|
||||
args.passphrase_ota_env_var,
|
||||
args.passphrase_ota_file,
|
||||
)
|
||||
|
||||
# Ensure that the certificate matches the private key
|
||||
if not openssl.cert_matches_key(args.cert_ota, args.privkey_ota,
|
||||
passphrase_ota):
|
||||
raise Exception('OTA certificate does not match private key')
|
||||
|
||||
start = time.perf_counter_ns()
|
||||
|
||||
with util.open_output_file(output) as temp_raw:
|
||||
with (
|
||||
ota.open_signing_wrapper(temp_raw, args.privkey_ota,
|
||||
passphrase_ota, args.cert_ota) as temp,
|
||||
ota.match_android_zip64_limit(),
|
||||
fix_streaming_local_header_sizes(),
|
||||
):
|
||||
context = PatchContext(
|
||||
replace_images=args.replace or {},
|
||||
boot_partition=args.boot_partition,
|
||||
root_patch=root_patch,
|
||||
clear_vbmeta_flags=args.clear_vbmeta_flags,
|
||||
privkey_avb=args.privkey_avb,
|
||||
passphrase_avb=passphrase_avb,
|
||||
privkey_ota=args.privkey_ota,
|
||||
passphrase_ota=passphrase_ota,
|
||||
cert_ota=args.cert_ota,
|
||||
)
|
||||
|
||||
metadata = patch_ota_zip(args.input, temp, context)
|
||||
|
||||
# We do a lot of low-level hackery. Reopen and verify offsets
|
||||
print_status('Verifying metadata offsets')
|
||||
with zipfile.ZipFile(temp_raw, 'r') as z:
|
||||
ota.verify_metadata(z, metadata)
|
||||
|
||||
# Excluding the time it takes for the user to type in the passwords
|
||||
elapsed = time.perf_counter_ns() - start
|
||||
print_status(f'Completed after {elapsed / 1_000_000_000:.1f}s')
|
||||
|
||||
|
||||
def extract_subcommand(args):
|
||||
with zipfile.ZipFile(args.input, 'r') as z:
|
||||
info = z.getinfo(PATH_PAYLOAD)
|
||||
|
||||
with z.open(info, 'r') as f:
|
||||
_, manifest, blob_offset = ota.parse_payload(f)
|
||||
|
||||
if args.all:
|
||||
unique_images = set(p.partition_name
|
||||
for p in manifest.partitions)
|
||||
else:
|
||||
images = get_required_images(manifest, args.boot_partition, True)
|
||||
if args.boot_only:
|
||||
unique_images = {images['@rootpatch']}
|
||||
else:
|
||||
unique_images = set(images.values())
|
||||
|
||||
print_status('Extracting', ', '.join(sorted(unique_images)),
|
||||
'from the payload')
|
||||
os.makedirs(args.directory, exist_ok=True)
|
||||
|
||||
# Extract in parallel. There's is no actual I/O parallelism due to
|
||||
# zipfile's internal locks, but this is still significantly faster than
|
||||
# doing it single threaded. The extraction process is mostly CPU board
|
||||
# due to decompression.
|
||||
ota.extract_images(lambda: z.open(info, 'r'),
|
||||
manifest, blob_offset, args.directory,
|
||||
unique_images)
|
||||
|
||||
|
||||
def magisk_info_subcommand(args):
|
||||
with open(args.image, 'rb') as f:
|
||||
img = bootimage.load_autodetect(f)
|
||||
|
||||
if not img.ramdisks:
|
||||
raise ValueError('Boot image does not have a ramdisk')
|
||||
|
||||
with (
|
||||
io.BytesIO(img.ramdisks[0]) as f_raw,
|
||||
compression.CompressedFile(f_raw, 'rb', raw_if_unknown=True) as f,
|
||||
):
|
||||
entries = cpio.load(f.fp)
|
||||
config = next((e for e in entries if e.name == b'.backup/.magisk'),
|
||||
None)
|
||||
if config is None:
|
||||
raise ValueError('Not a Magisk-patched boot image')
|
||||
|
||||
print(config.content.decode('ascii'), end='')
|
||||
|
||||
|
||||
def uint64_arg(arg):
|
||||
value = int(arg)
|
||||
if value < 0 or value >= 2 ** 64:
|
||||
raise ValueError('Out of range for unsigned 64-bit integer')
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class KeyValuePairAction(argparse.Action):
|
||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||
if nargs != 2:
|
||||
raise ValueError('nargs must be 2')
|
||||
|
||||
super().__init__(option_strings, dest, nargs=nargs, **kwargs)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
data = getattr(namespace, self.dest, None)
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
data[values[0]] = values[1]
|
||||
setattr(namespace, self.dest, data)
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(
|
||||
dest='subcommand',
|
||||
required=True,
|
||||
help='Subcommands',
|
||||
)
|
||||
|
||||
patch = subparsers.add_parser(
|
||||
'patch',
|
||||
help='Patch a full OTA zip',
|
||||
)
|
||||
|
||||
patch.add_argument(
|
||||
'--input',
|
||||
required=True,
|
||||
help='Path to original raw payload or OTA zip',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--output',
|
||||
help='Path to new raw payload or OTA zip',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--privkey-avb',
|
||||
required=True,
|
||||
help='Private key for signing root vbmeta image',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--privkey-ota',
|
||||
required=True,
|
||||
help='Private key for signing OTA payload',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--cert-ota',
|
||||
required=True,
|
||||
help='Certificate for OTA payload signing key',
|
||||
)
|
||||
|
||||
for arg in ('AVB', 'OTA'):
|
||||
group = patch.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
f'--passphrase-{arg.lower()}-env-var',
|
||||
help=f'Environment variable containing {arg} private key passphrase',
|
||||
)
|
||||
group.add_argument(
|
||||
f'--passphrase-{arg.lower()}-file',
|
||||
help=f'File containing {arg} private key passphrase',
|
||||
)
|
||||
|
||||
patch.add_argument(
|
||||
'--replace',
|
||||
nargs=2,
|
||||
action=KeyValuePairAction,
|
||||
help='Use partition image from a file instead of the original payload',
|
||||
)
|
||||
|
||||
boot_group = patch.add_mutually_exclusive_group(required=True)
|
||||
boot_group.add_argument(
|
||||
'--magisk',
|
||||
help='Path to Magisk APK',
|
||||
)
|
||||
boot_group.add_argument(
|
||||
'--prepatched',
|
||||
help='Path to prepatched boot image',
|
||||
)
|
||||
boot_group.add_argument(
|
||||
'--rootless',
|
||||
action='store_true',
|
||||
help='Skip applying root patch',
|
||||
)
|
||||
|
||||
patch.add_argument(
|
||||
'--magisk-preinit-device',
|
||||
help='Magisk preinit device',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--magisk-random-seed',
|
||||
type=uint64_arg,
|
||||
help='Magisk random seed',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--ignore-magisk-warnings',
|
||||
action='store_true',
|
||||
help='Ignore Magisk compatibility/version warnings',
|
||||
)
|
||||
patch.add_argument(
|
||||
'--ignore-prepatched-compat',
|
||||
default=0,
|
||||
action='count',
|
||||
help='Ignore compatibility issues with prepatched boot images',
|
||||
)
|
||||
|
||||
patch.add_argument(
|
||||
'--clear-vbmeta-flags',
|
||||
action='store_true',
|
||||
help='Forcibly clear vbmeta flags if they disable AVB',
|
||||
)
|
||||
|
||||
extract = subparsers.add_parser(
|
||||
'extract',
|
||||
help='Extract patched images from a patched OTA zip',
|
||||
)
|
||||
|
||||
extract.add_argument(
|
||||
'--input',
|
||||
required=True,
|
||||
help='Path to patched OTA zip',
|
||||
)
|
||||
extract.add_argument(
|
||||
'--directory',
|
||||
default='.',
|
||||
help='Output directory for extracted images',
|
||||
)
|
||||
extract_group = extract.add_mutually_exclusive_group()
|
||||
extract_group.add_argument(
|
||||
'--all',
|
||||
action='store_true',
|
||||
help='Extract all images from the payload',
|
||||
)
|
||||
extract_group.add_argument(
|
||||
'--boot-only',
|
||||
action='store_true',
|
||||
help='Extract only the boot image',
|
||||
)
|
||||
|
||||
for subcmd in (patch, extract):
|
||||
subcmd.add_argument(
|
||||
'--boot-partition',
|
||||
default='@gki_ramdisk',
|
||||
help='Boot partition name',
|
||||
)
|
||||
|
||||
magisk_info = subparsers.add_parser(
|
||||
'magisk-info',
|
||||
help='Print Magisk config from a patched boot image',
|
||||
)
|
||||
magisk_info.add_argument(
|
||||
'--image',
|
||||
required=True,
|
||||
help='Patch to Magisk-patched boot image',
|
||||
)
|
||||
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
if args.subcommand == 'patch':
|
||||
if args.magisk is None:
|
||||
if args.magisk_preinit_device:
|
||||
parser.error('--magisk-preinit-device requires --magisk')
|
||||
elif args.magisk_random_seed:
|
||||
parser.error('--magisk-random-seed requires --magisk')
|
||||
elif args.ignore_magisk_warnings:
|
||||
parser.error('--ignore-magisk-warnings requires --magisk')
|
||||
elif args.prepatched is None:
|
||||
if args.ignore_prepatched_compat:
|
||||
parser.error('--ignore-prepatched-compat requires --prepatched')
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv=argv)
|
||||
|
||||
util.load_umask_unsafe()
|
||||
|
||||
if args.subcommand == 'patch':
|
||||
patch_subcommand(args)
|
||||
elif args.subcommand == 'extract':
|
||||
extract_subcommand(args)
|
||||
elif args.subcommand == 'magisk-info':
|
||||
magisk_info_subcommand(args)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
@@ -1,222 +0,0 @@
|
||||
import binascii
|
||||
import contextlib
|
||||
import getpass
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
import unittest.mock
|
||||
|
||||
# This module calls the openssl binary because AOSP's avbtool.py already does
|
||||
# that and the operations are simple enough to not require pulling in a
|
||||
# library.
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _passphrase_fd(passphrase):
|
||||
'''
|
||||
If the specified passphrase is not None, yield the readable end of a pipe
|
||||
that produces the passphrase encoded as UTF-8, followed by a newline. The
|
||||
read end of the pipe is marked as inheritable. Both ends of the pipe are
|
||||
closed after leaving the context.
|
||||
'''
|
||||
|
||||
assert os.name != 'nt'
|
||||
|
||||
if passphrase is None:
|
||||
yield None
|
||||
return
|
||||
|
||||
# For simplicity, we don't write to the pipe on a thread, so pick a maximum
|
||||
# length that doesn't exceed any OS's pipe buffer size, while still being
|
||||
# usable for just about every use case.
|
||||
if len(passphrase) >= 4096:
|
||||
raise ValueError('Passphrase is too long')
|
||||
|
||||
pipe_r, pipe_w = os.pipe()
|
||||
write_closed = False
|
||||
|
||||
try:
|
||||
os.set_inheritable(pipe_r, True)
|
||||
|
||||
os.write(pipe_w, passphrase.encode('UTF-8'))
|
||||
os.write(pipe_w, b'\n')
|
||||
os.close(pipe_w)
|
||||
write_closed = True
|
||||
|
||||
yield pipe_r
|
||||
finally:
|
||||
os.close(pipe_r)
|
||||
if not write_closed:
|
||||
os.close(pipe_w)
|
||||
|
||||
|
||||
class _PopenPassphraseWrapper:
|
||||
'''
|
||||
Wrapper around subprocess.Popen() that adds arguments for passing in the
|
||||
private key passphrase via a pipe on non-Windows systems. On Windows,
|
||||
openssl does not support reading from pipes, so the passphrase is passed in
|
||||
via an environment variable.
|
||||
'''
|
||||
|
||||
def __init__(self, passphrase):
|
||||
self.orig_popen = subprocess.Popen
|
||||
self.passphrase = passphrase
|
||||
|
||||
def __call__(self, cmd, *args, **kwargs):
|
||||
if self.passphrase is not None and cmd and \
|
||||
os.path.basename(cmd[0]) == 'openssl':
|
||||
if os.name == 'nt':
|
||||
# On Windows, opensssl does not support reading the passphrase
|
||||
# from a file descriptor. An environment variable is the next
|
||||
# best way to handle this.
|
||||
if 'env' not in kwargs:
|
||||
kwargs['env'] = dict(os.environ)
|
||||
|
||||
env_var = ''.join(random.choices(string.ascii_letters, k=64))
|
||||
kwargs['env'][env_var] = self.passphrase
|
||||
|
||||
new_cmd = [*cmd, '-passin', f'env:{env_var}']
|
||||
|
||||
return self.orig_popen(new_cmd, *args, **kwargs)
|
||||
else:
|
||||
with _passphrase_fd(self.passphrase) as fd:
|
||||
kwargs['close_fds'] = False
|
||||
|
||||
new_cmd = [*cmd, '-passin', f'fd:{fd}']
|
||||
|
||||
return self.orig_popen(new_cmd, *args, **kwargs)
|
||||
|
||||
# The pipe is closed at this point in this process, but the
|
||||
# child already inherited the fd and the passphrase is sitting
|
||||
# the pipe buffer.
|
||||
else:
|
||||
return self.orig_popen(cmd, *args, **kwargs)
|
||||
|
||||
|
||||
def inject_passphrase(passphrase):
|
||||
'''
|
||||
While this context is active, patch subprocess calls to openssl so that
|
||||
the passphrase is specified via an injected -passin argument, if it is not
|
||||
None. The passphrase is passed to the command via a pipe file descriptor
|
||||
(non-Windows) or an environment variable (Windows).
|
||||
'''
|
||||
|
||||
return unittest.mock.patch(
|
||||
'subprocess.Popen', side_effect=_PopenPassphraseWrapper(passphrase))
|
||||
|
||||
|
||||
def _guess_format(path):
|
||||
'''
|
||||
Simple heuristic to determine the encoding of a key. This is needed because
|
||||
openssl 1.1 doesn't support autodetection.
|
||||
'''
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
for line in f:
|
||||
if line.startswith(b'-----BEGIN '):
|
||||
return 'PEM'
|
||||
|
||||
return 'DER'
|
||||
|
||||
|
||||
def _get_modulus(path, passphrase, is_x509):
|
||||
'''
|
||||
Get the RSA modulus of the given file, which can be a private key or
|
||||
certificate.
|
||||
'''
|
||||
|
||||
with inject_passphrase(passphrase):
|
||||
output = subprocess.check_output([
|
||||
'openssl',
|
||||
'x509' if is_x509 else 'rsa',
|
||||
'-in', path,
|
||||
'-inform', _guess_format(path),
|
||||
'-noout',
|
||||
'-modulus',
|
||||
])
|
||||
|
||||
prefix, delim, suffix = output.strip().partition(b'=')
|
||||
if not delim or prefix != b'Modulus':
|
||||
raise Exception(f'Unexpected modulus output: {repr(output)}')
|
||||
|
||||
return binascii.unhexlify(suffix)
|
||||
|
||||
|
||||
def max_signature_size(pkey, passphrase):
|
||||
'''
|
||||
Get the maximum size of a signature signed by the specified RSA key. This
|
||||
is equal to the modulus size.
|
||||
'''
|
||||
|
||||
return len(_get_modulus(pkey, passphrase, False))
|
||||
|
||||
|
||||
def sign_data(pkey, passphrase, data):
|
||||
'''
|
||||
Sign <data> with <pkey>.
|
||||
'''
|
||||
|
||||
with inject_passphrase(passphrase):
|
||||
return subprocess.check_output(
|
||||
[
|
||||
'openssl', 'pkeyutl',
|
||||
'-sign',
|
||||
'-inkey', pkey,
|
||||
'-keyform', _guess_format(pkey),
|
||||
'-pkeyopt', 'digest:sha256',
|
||||
],
|
||||
input=data,
|
||||
)
|
||||
|
||||
|
||||
def cert_matches_key(cert, pkey, passphrase):
|
||||
'''
|
||||
Check that the x509 certificate matches the RSA private key.
|
||||
'''
|
||||
|
||||
return _get_modulus(cert, None, True) \
|
||||
== _get_modulus(pkey, passphrase, False)
|
||||
|
||||
|
||||
def _is_encrypted(pkey):
|
||||
'''
|
||||
Check if a private key is encrypted.
|
||||
'''
|
||||
|
||||
with open(pkey, 'rb') as f:
|
||||
for line in f:
|
||||
if b'-----BEGIN ENCRYPTED PRIVATE KEY-----' == line.strip():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def prompt_passphrase(pkey, passphrase_env_var=None, passphrase_file=None):
|
||||
'''
|
||||
If the private key is encrypted:
|
||||
|
||||
* try to read from the specified passphrase file (first line with trailing
|
||||
line endings stripped)
|
||||
* try to read from the passphrase environment variable
|
||||
* prompt for the passphrase interactively
|
||||
|
||||
There is no fallback behavior.
|
||||
'''
|
||||
|
||||
if not _is_encrypted(pkey):
|
||||
return None
|
||||
|
||||
if passphrase_file is not None:
|
||||
with open(passphrase_file, 'r') as f:
|
||||
passphrase = f.readline().rstrip('\r\n')
|
||||
elif passphrase_env_var is not None:
|
||||
passphrase = os.environ[passphrase_env_var]
|
||||
else:
|
||||
passphrase = getpass.getpass(f'Passphrase for {pkey}: ')
|
||||
|
||||
# Verify that it is correct
|
||||
with inject_passphrase(passphrase):
|
||||
subprocess.check_output(['openssl', 'pkey', '-in', pkey, '-noout'])
|
||||
|
||||
return passphrase
|
||||
-817
@@ -1,817 +0,0 @@
|
||||
import base64
|
||||
import binascii
|
||||
import bz2
|
||||
import collections
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import lzma
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import unittest.mock
|
||||
import zipfile
|
||||
|
||||
# Silence undesired warning
|
||||
orig_argv0 = sys.argv[0]
|
||||
sys.argv[0] = os.path.basename(sys.argv[0]).removesuffix('.py')
|
||||
import ota_utils
|
||||
sys.argv[0] = orig_argv0
|
||||
|
||||
import ota_metadata_pb2
|
||||
import update_metadata_pb2
|
||||
|
||||
from . import openssl
|
||||
from . import util
|
||||
|
||||
|
||||
OTA_MAGIC = b'CrAU'
|
||||
|
||||
|
||||
def parse_payload(f):
|
||||
'''
|
||||
Parse payload header from a file-like object. After this function returns,
|
||||
the file position is set to the beginning of the blob section.
|
||||
'''
|
||||
|
||||
f.seek(0)
|
||||
|
||||
# Validate header
|
||||
magic = f.read(4)
|
||||
if magic != OTA_MAGIC:
|
||||
raise Exception(f'Invalid magic: {magic}')
|
||||
|
||||
version, = struct.unpack('!Q', f.read(8))
|
||||
if version != 2:
|
||||
raise Exception(f'Unsupported version: {version}')
|
||||
|
||||
manifest_size, = struct.unpack('!Q', f.read(8))
|
||||
metadata_signature_size, = struct.unpack('!I', f.read(4))
|
||||
|
||||
# Read manifest
|
||||
manifest_raw = f.read(manifest_size)
|
||||
manifest = update_metadata_pb2.DeltaArchiveManifest()
|
||||
manifest.ParseFromString(manifest_raw)
|
||||
|
||||
if any(p.HasField('old_partition_info') for p in manifest.partitions):
|
||||
raise Exception('File is a delta OTA, not a full OTA')
|
||||
|
||||
# Skip manifest signatures
|
||||
f.seek(metadata_signature_size, os.SEEK_CUR)
|
||||
|
||||
return (version, manifest, f.tell())
|
||||
|
||||
|
||||
def _extract_image(f_payload, f_out, block_size, blob_offset, partition,
|
||||
cancel_signal):
|
||||
'''
|
||||
Extract the partition image from <f_payload> to <f_out> by processing the
|
||||
manifests list of install operations.
|
||||
'''
|
||||
|
||||
Type = update_metadata_pb2.InstallOperation.Type
|
||||
|
||||
for op in partition.operations:
|
||||
for extent in op.dst_extents:
|
||||
if cancel_signal.is_set():
|
||||
raise Exception('Interrupted')
|
||||
|
||||
f_payload.seek(blob_offset + op.data_offset)
|
||||
f_out.seek(extent.start_block * block_size)
|
||||
h_data = hashlib.sha256()
|
||||
|
||||
if op.type == Type.REPLACE:
|
||||
util.copyfileobj_n(f_payload, f_out, op.data_length,
|
||||
hasher=h_data)
|
||||
elif op.type == Type.REPLACE_BZ:
|
||||
decompressor = bz2.BZ2Decompressor()
|
||||
util.decompress_n(decompressor, f_payload, f_out,
|
||||
op.data_length, hasher=h_data)
|
||||
elif op.type == Type.REPLACE_XZ:
|
||||
decompressor = lzma.LZMADecompressor()
|
||||
util.decompress_n(decompressor, f_payload, f_out,
|
||||
op.data_length, hasher=h_data)
|
||||
elif op.type == Type.ZERO or op.type == Type.DISCARD:
|
||||
util.zero_n(f_out, extent.num_blocks * block_size)
|
||||
else:
|
||||
raise Exception(f'Unsupported operation: {op.type}')
|
||||
|
||||
if h_data.digest() != op.data_sha256_hash and op.type != Type.ZERO:
|
||||
raise Exception('Expected hash %s, but got %s' %
|
||||
(h_data.hexdigest(),
|
||||
binascii.hexlify(op.data_sha256_hash)))
|
||||
|
||||
|
||||
def extract_images(f, manifest, blob_offset, output_dir, partition_names):
|
||||
'''
|
||||
Extract the specified partition images from the payload into <output_dir>.
|
||||
|
||||
If <f> is callable, then it should produce a new file object each time it
|
||||
is called. This allows extracting images in parallel.
|
||||
'''
|
||||
|
||||
remaining = set(partition_names)
|
||||
max_workers = len(remaining)
|
||||
cancel_signal = threading.Event()
|
||||
futures = []
|
||||
|
||||
if not callable(f):
|
||||
f_orig = f
|
||||
|
||||
@contextlib.contextmanager
|
||||
def dummy():
|
||||
yield f_orig
|
||||
|
||||
f = dummy
|
||||
max_workers = 1
|
||||
|
||||
def extract(p):
|
||||
output_path = os.path.join(output_dir, p.partition_name + '.img')
|
||||
|
||||
with (
|
||||
f() as f_in,
|
||||
open(output_path, 'wb') as f_out,
|
||||
):
|
||||
_extract_image(f_in, f_out, manifest.block_size, blob_offset, p,
|
||||
cancel_signal)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=max_workers) as executor:
|
||||
try:
|
||||
for p in manifest.partitions:
|
||||
if p.partition_name not in remaining:
|
||||
continue
|
||||
|
||||
remaining.remove(p.partition_name)
|
||||
|
||||
futures.append(executor.submit(extract, p))
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
future.result()
|
||||
except BaseException:
|
||||
cancel_signal.set()
|
||||
raise
|
||||
|
||||
if remaining:
|
||||
raise Exception(f'Images not found: {remaining}')
|
||||
|
||||
|
||||
def _compress_image(partition, block_size, input_path, output_path):
|
||||
'''
|
||||
XZ-compress the image at <input_path> to <output_path> and update the
|
||||
partition metadata with the appropriate checksums and install operations
|
||||
metadata.
|
||||
|
||||
The size in the (sole) install operation is set correctly, but the offset
|
||||
must be manually updated. It is initially set to the maximum uint64 value.
|
||||
'''
|
||||
|
||||
h_uncompressed = hashlib.sha256()
|
||||
h_compressed = hashlib.sha256()
|
||||
size_uncompressed = 0
|
||||
size_compressed = 0
|
||||
# AOSP's payload_consumer does not support CRC during decompression
|
||||
compressor = lzma.LZMACompressor(check=lzma.CHECK_NONE)
|
||||
buf = bytearray(16384)
|
||||
buf_view = memoryview(buf)
|
||||
|
||||
with (
|
||||
open(input_path, 'rb', buffering=0) as f_in,
|
||||
open(output_path, 'wb') as f_out,
|
||||
):
|
||||
while n := f_in.readinto(buf_view):
|
||||
h_uncompressed.update(buf_view[:n])
|
||||
size_uncompressed += n
|
||||
|
||||
xz_data = compressor.compress(buf_view[:n])
|
||||
h_compressed.update(xz_data)
|
||||
size_compressed += len(xz_data)
|
||||
f_out.write(xz_data)
|
||||
|
||||
xz_data = compressor.flush()
|
||||
h_compressed.update(xz_data)
|
||||
size_compressed += len(xz_data)
|
||||
f_out.write(xz_data)
|
||||
|
||||
if size_uncompressed % block_size:
|
||||
raise Exception('Size of %s (%d) is not aligned to the block size (%d)'
|
||||
% (partition.partition_name, size_uncompressed,
|
||||
block_size))
|
||||
|
||||
partition.new_partition_info.size = size_uncompressed
|
||||
partition.new_partition_info.hash = h_uncompressed.digest()
|
||||
|
||||
extent = update_metadata_pb2.Extent()
|
||||
extent.start_block = 0
|
||||
extent.num_blocks = size_uncompressed // block_size
|
||||
|
||||
operation = update_metadata_pb2.InstallOperation()
|
||||
operation.type = update_metadata_pb2.InstallOperation.Type.REPLACE_XZ
|
||||
# Must be manually updated by the caller
|
||||
operation.data_offset = 2 ** 64 - 1
|
||||
operation.data_length = size_compressed
|
||||
operation.dst_extents.append(extent)
|
||||
operation.data_sha256_hash = h_compressed.digest()
|
||||
|
||||
partition.ClearField('operations')
|
||||
partition.operations.append(operation)
|
||||
|
||||
|
||||
def _recompute_offsets(manifest, new_images):
|
||||
'''
|
||||
Recompute the blob offsets to account for the new images.
|
||||
|
||||
Returns ([(<image file>, <data offset>, <data size>)], <blob size>). If the
|
||||
image file is None, then the data offset is relative to the blob offset of
|
||||
the original payload. Otherwise, the data offset is an absolute offset into
|
||||
the image file.
|
||||
'''
|
||||
|
||||
# (<image file>, <data offset>, <data size>)
|
||||
data_list = []
|
||||
offset = 0
|
||||
|
||||
for p in manifest.partitions:
|
||||
is_patched = p.partition_name in new_images
|
||||
p_offset = 0
|
||||
|
||||
for op in p.operations:
|
||||
if is_patched:
|
||||
data_list.append((
|
||||
new_images[p.partition_name],
|
||||
p_offset,
|
||||
op.data_length,
|
||||
))
|
||||
else:
|
||||
data_list.append((
|
||||
None,
|
||||
op.data_offset,
|
||||
op.data_length,
|
||||
))
|
||||
|
||||
op.data_offset = offset
|
||||
p_offset += op.data_length
|
||||
offset += op.data_length
|
||||
|
||||
return (data_list, offset)
|
||||
|
||||
|
||||
def _sign_hash(hash, key, passphrase, max_sig_size):
|
||||
'''
|
||||
Sign <hash> with <key> and return a Signatures protobuf struct with the
|
||||
signature padded to <max_sig_size>.
|
||||
'''
|
||||
|
||||
hash_signed = openssl.sign_data(key, passphrase, hash)
|
||||
assert len(hash_signed) <= max_sig_size
|
||||
|
||||
signature = update_metadata_pb2.Signatures.Signature()
|
||||
signature.unpadded_signature_size = len(hash_signed)
|
||||
signature.data = hash_signed + b'\0' * (max_sig_size - len(hash_signed))
|
||||
|
||||
signatures = update_metadata_pb2.Signatures()
|
||||
signatures.signatures.append(signature)
|
||||
|
||||
return signatures
|
||||
|
||||
|
||||
def _serialize_protobuf(p):
|
||||
return p.SerializeToString(deterministic=True)
|
||||
|
||||
|
||||
def patch_payload(f_in, f_out, version, manifest, blob_offset, temp_dir,
|
||||
patched, file_size, key, passphrase):
|
||||
'''
|
||||
Copy the payload from <f_in> to <f_out>, updating references to <patched>
|
||||
images as they are encountered. <f_out> will be signed with <key>.
|
||||
'''
|
||||
|
||||
max_sig_size = openssl.max_signature_size(key, passphrase)
|
||||
|
||||
# Strip out old payload signature
|
||||
if manifest.HasField('signatures_size'):
|
||||
trunc_file_size = blob_offset + manifest.signatures_offset
|
||||
if trunc_file_size > file_size:
|
||||
raise Exception('Payload signature offset is beyond EOF')
|
||||
file_size = trunc_file_size
|
||||
|
||||
# Partition name -> compressed image path
|
||||
compressed = {}
|
||||
|
||||
# Update the partition manifests to refer to the patched images
|
||||
for name, path in patched.items():
|
||||
# Find the partition in the manifest
|
||||
partition = next((p for p in manifest.partitions
|
||||
if p.partition_name == name), None)
|
||||
if partition is None:
|
||||
raise Exception(f'Partition {name} not found in manifest')
|
||||
|
||||
# Compress the image and update the partition manifest accordingly
|
||||
compressed_path = os.path.join(temp_dir, f'{name}.img')
|
||||
_compress_image(
|
||||
partition,
|
||||
manifest.block_size,
|
||||
path,
|
||||
compressed_path,
|
||||
)
|
||||
compressed[name] = compressed_path
|
||||
|
||||
# Fill out blob offsets and compute final size
|
||||
blob_data_list, blob_size = _recompute_offsets(manifest, compressed)
|
||||
|
||||
# Get the length of an dummy signature struct since the length fields are
|
||||
# part of the data to be signed
|
||||
dummy_sig = _sign_hash(hashlib.sha256().digest(), key, passphrase,
|
||||
max_sig_size)
|
||||
dummy_sig_size = len(_serialize_protobuf(dummy_sig))
|
||||
|
||||
# Fill out new payload signature information
|
||||
manifest.signatures_offset = blob_size
|
||||
manifest.signatures_size = dummy_sig_size
|
||||
|
||||
# Build new manifest
|
||||
manifest_raw_new = _serialize_protobuf(manifest)
|
||||
|
||||
class MultipleHasher:
|
||||
def __init__(self, hashers):
|
||||
self.hashers = hashers
|
||||
|
||||
def update(self, data):
|
||||
for hasher in self.hashers:
|
||||
hasher.update(data)
|
||||
|
||||
# Excludes signatures (hashes are for signing)
|
||||
h_partial = hashlib.sha256()
|
||||
# Includes signatures (hashes are for properties file)
|
||||
h_full = hashlib.sha256()
|
||||
# Updates both of the above
|
||||
h_both = MultipleHasher((h_partial, h_full))
|
||||
|
||||
def write(hasher, data):
|
||||
hasher.update(data)
|
||||
f_out.write(data)
|
||||
|
||||
# Write header to output file
|
||||
write(h_both, OTA_MAGIC)
|
||||
write(h_both, struct.pack('!Q', version))
|
||||
write(h_both, struct.pack('!Q', len(manifest_raw_new)))
|
||||
write(h_both, struct.pack('!I', dummy_sig_size))
|
||||
|
||||
# Write new manifest
|
||||
write(h_both, manifest_raw_new)
|
||||
|
||||
# Sign metadata (header + manifest) hash. The signature is not included in
|
||||
# the payload hash.
|
||||
metadata_hash = h_partial.digest()
|
||||
metadata_sig = _sign_hash(metadata_hash, key, passphrase, max_sig_size)
|
||||
write(h_full, _serialize_protobuf(metadata_sig))
|
||||
|
||||
# Write new blob
|
||||
for image_file, data_offset, data_length in blob_data_list:
|
||||
if image_file is None:
|
||||
f_in.seek(blob_offset + data_offset)
|
||||
util.copyfileobj_n(f_in, f_out, data_length, hasher=h_both)
|
||||
else:
|
||||
with open(image_file, 'rb') as f_image:
|
||||
f_image.seek(data_offset)
|
||||
util.copyfileobj_n(f_image, f_out, data_length, hasher=h_both)
|
||||
|
||||
# Append payload signature
|
||||
payload_sig = _sign_hash(h_partial.digest(), key, passphrase, max_sig_size)
|
||||
write(h_full, _serialize_protobuf(payload_sig))
|
||||
|
||||
# Generate properties file
|
||||
metadata_offset = len(OTA_MAGIC) + struct.calcsize('!QQI')
|
||||
metadata_size = metadata_offset + len(manifest_raw_new)
|
||||
blob_size = manifest.signatures_offset + manifest.signatures_size
|
||||
new_file_size = metadata_size + dummy_sig_size + blob_size
|
||||
|
||||
def b64(d): return base64.b64encode(d)
|
||||
props = [
|
||||
b'FILE_HASH=%s\n' % b64(h_full.digest()),
|
||||
b'FILE_SIZE=%d\n' % new_file_size,
|
||||
b'METADATA_HASH=%s\n' % b64(metadata_hash),
|
||||
b'METADATA_SIZE=%d\n' % metadata_size,
|
||||
]
|
||||
|
||||
return b''.join(props)
|
||||
|
||||
|
||||
def _get_property_files():
|
||||
'''
|
||||
Return the set of property files to add to the OTA metadata files.
|
||||
'''
|
||||
|
||||
return (
|
||||
ota_utils.AbOtaPropertyFiles(),
|
||||
ota_utils.StreamingPropertyFiles(),
|
||||
)
|
||||
|
||||
|
||||
def _serialize_metadata(metadata):
|
||||
'''
|
||||
Generate the legacy plain-text and protobuf serializations of the given
|
||||
metadata instance.
|
||||
'''
|
||||
|
||||
legacy_metadata = ota_utils.BuildLegacyOtaMetadata(metadata)
|
||||
legacy_metadata_str = "".join([f'{k}={v}\n' for k, v in
|
||||
sorted(legacy_metadata.items())])
|
||||
metadata_bytes = _serialize_protobuf(metadata)
|
||||
|
||||
return legacy_metadata_str.encode('UTF-8'), metadata_bytes
|
||||
|
||||
|
||||
_FileRange = collections.namedtuple(
|
||||
'_FileRange', ('start', 'end', 'data_or_fp'))
|
||||
|
||||
|
||||
class _ConcatenatedFileDescriptor:
|
||||
'''
|
||||
A read-only seekable file descriptor that presents several file descriptors
|
||||
or byte arrays as a single concatenated file.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
# List of (start, end, data_or_fp)
|
||||
self.ranges = []
|
||||
self.offset = 0
|
||||
|
||||
def _get_range(self):
|
||||
for range in self.ranges:
|
||||
if self.offset >= range.start and self.offset < range.end:
|
||||
return range
|
||||
|
||||
return None
|
||||
|
||||
def _eof_offset(self):
|
||||
return self.ranges[-1].end if self.ranges else 0
|
||||
|
||||
def add_file(self, fp):
|
||||
start = self._eof_offset()
|
||||
self.ranges.append(_FileRange(start, start + fp.tell(), fp))
|
||||
|
||||
def add_bytes(self, data):
|
||||
start = self._eof_offset()
|
||||
self.ranges.append(_FileRange(start, start + len(data), data))
|
||||
|
||||
def read(self, size=None):
|
||||
buf = b''
|
||||
|
||||
while size is None or size > 0:
|
||||
range = self._get_range()
|
||||
if not range:
|
||||
break
|
||||
|
||||
to_read = range.end - self.offset
|
||||
if size is not None:
|
||||
to_read = min(to_read, size)
|
||||
data_offset = self.offset - range.start
|
||||
|
||||
if isinstance(range.data_or_fp, bytes):
|
||||
data = range.data_or_fp[data_offset:data_offset + to_read]
|
||||
else:
|
||||
range.data_or_fp.seek(data_offset)
|
||||
data = range.data_or_fp.read(to_read)
|
||||
|
||||
if not buf:
|
||||
buf = data
|
||||
else:
|
||||
buf += data
|
||||
|
||||
if len(data) < to_read:
|
||||
if range is not self.ranges[-1]:
|
||||
raise Exception('Unexpected EOF')
|
||||
else:
|
||||
break
|
||||
|
||||
if size is not None:
|
||||
size -= to_read
|
||||
|
||||
return buf
|
||||
|
||||
def seek(self, offset, whence=os.SEEK_SET):
|
||||
if whence == os.SEEK_SET:
|
||||
self.offset = offset
|
||||
elif whence == os.SEEK_CUR:
|
||||
self.offset += offset
|
||||
elif whence == os.SEEK_END:
|
||||
self.offset = self._eof_offset() + offset
|
||||
else:
|
||||
raise ValueError(f'Invalid whence: {whence}')
|
||||
|
||||
def tell(self):
|
||||
return self.offset
|
||||
|
||||
|
||||
class _MemoryFile(io.BytesIO):
|
||||
'''
|
||||
Subclass of io.BytesIO where seeking can be conditionally disabled.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, allow_seek=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.allow_seek = allow_seek
|
||||
|
||||
def seek(self, *args, **kwargs):
|
||||
if not self.allow_seek:
|
||||
raise AttributeError('seek is not supported')
|
||||
|
||||
return super().seek(*args, **kwargs)
|
||||
|
||||
|
||||
class _FakeZipFile:
|
||||
'''
|
||||
A wrapper around a ZipFile instance that allows appending new entries in
|
||||
memory without modifying the backing file.
|
||||
|
||||
NOTE: The underlying ZipFile's file descriptor's position may be changed.
|
||||
'''
|
||||
|
||||
def __init__(self, z):
|
||||
self.zip = z
|
||||
|
||||
self.fp = _ConcatenatedFileDescriptor()
|
||||
|
||||
# We have a seekable underlying file descriptor to the zip, but we
|
||||
# intentionally don't allow _TeeFileDescriptor to be seekable to
|
||||
# guarantee that ZipFile writes sequentially.
|
||||
self.orig_fp = self.zip.fp
|
||||
if isinstance(self.orig_fp, _TeeFileDescriptor):
|
||||
self.orig_fp = self.orig_fp.backing
|
||||
|
||||
self.fp.add_file(self.orig_fp)
|
||||
|
||||
self.next_offset = self.zip.start_dir
|
||||
|
||||
self.extra_infos = {}
|
||||
|
||||
def getinfo(self, name):
|
||||
if name in self.extra_infos:
|
||||
return self.extra_infos[name]
|
||||
else:
|
||||
return self.zip.getinfo(name)
|
||||
|
||||
def namelist(self):
|
||||
return self.zip.namelist() + list(self.extra_infos.keys())
|
||||
|
||||
def add_file(self, info, data):
|
||||
# Disable seeking to ensure that data descriptors are written, like the
|
||||
# backing ZipFile
|
||||
with _MemoryFile(allow_seek=False) as mem:
|
||||
with zipfile.ZipFile(mem, 'w') as z:
|
||||
with z.open(info, 'w') as f:
|
||||
f.write(data)
|
||||
|
||||
# Capture local file header, data, and data descriptor
|
||||
buf_without_footer = mem.getvalue()
|
||||
self.fp.add_bytes(buf_without_footer)
|
||||
|
||||
# Fix offset and add to fake entries
|
||||
new_info = z.infolist()[-1]
|
||||
new_info.header_offset = self.next_offset
|
||||
self.extra_infos[new_info.filename] = new_info
|
||||
|
||||
self.next_offset += len(buf_without_footer)
|
||||
|
||||
|
||||
def add_metadata(z_out, metadata_info, metadata_pb_info, metadata_pb_raw):
|
||||
'''
|
||||
Add metadata files to the output OTA zip. <metadata_info> and
|
||||
<metadata_pb_info> should be the ZipInfo instances associated with the
|
||||
files from the original OTA zip. <metadata_pb_raw> should be the serialized
|
||||
OTA metadata protobuf struct from the original OTA.
|
||||
|
||||
The zip file's backing file position MUST BE set to where the central
|
||||
directory would start.
|
||||
'''
|
||||
|
||||
metadata = ota_metadata_pb2.OtaMetadata()
|
||||
metadata.ParseFromString(metadata_pb_raw)
|
||||
|
||||
metadata.property_files.clear()
|
||||
|
||||
props = _get_property_files()
|
||||
|
||||
# Create a fake zip instance that allows appending new entries in memory so
|
||||
# that ota_utils can compute offsets for the property files
|
||||
fake_zip = _FakeZipFile(z_out)
|
||||
|
||||
# Compute initial property files with reserved space as placeholders to
|
||||
# store the self-referential metadata entries later
|
||||
for p in props:
|
||||
metadata.property_files[p.name] = p.Compute(fake_zip)
|
||||
|
||||
# Add the placeholders to the fake zip to compute final property files
|
||||
new_metadata_raw, new_metadata_pb_raw = _serialize_metadata(metadata)
|
||||
fake_zip.add_file(metadata_info, new_metadata_raw)
|
||||
fake_zip.add_file(metadata_pb_info, new_metadata_pb_raw)
|
||||
|
||||
# Compute the final property files using the offsets of the fake entries
|
||||
for p in props:
|
||||
metadata.property_files[p.name] = \
|
||||
p.Finalize(fake_zip, len(metadata.property_files[p.name]))
|
||||
|
||||
# Offset computation changes the file offset of the actual file. Seek back
|
||||
# to where the next entry or central directory would go
|
||||
fake_zip.orig_fp.seek(z_out.start_dir)
|
||||
|
||||
# Add the final metadata files to the real zip
|
||||
new_metadata_raw, new_metadata_pb_raw = _serialize_metadata(metadata)
|
||||
with z_out.open(metadata_info, 'w') as f:
|
||||
f.write(new_metadata_raw)
|
||||
with z_out.open(metadata_pb_info, 'w') as f:
|
||||
f.write(new_metadata_pb_raw)
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def verify_metadata(z, metadata):
|
||||
'''
|
||||
Verify that the offsets and file sizes within the metadata file properties
|
||||
of a fully written OTA zip are correct.
|
||||
'''
|
||||
|
||||
for p in _get_property_files():
|
||||
p.Verify(z, metadata.property_files[p.name].strip())
|
||||
|
||||
|
||||
class _TeeFileDescriptor:
|
||||
'''
|
||||
A file-like instance that propagates writes to multiple streams.
|
||||
|
||||
start_capture() is used to pause output and divert writes to a memory
|
||||
buffer until _finish_capture(), which can modify the buffer.
|
||||
'''
|
||||
|
||||
def __init__(self, streams, file_index=None):
|
||||
self.streams = streams
|
||||
self.capture = None
|
||||
self.backing = None if file_index is None else streams[file_index]
|
||||
|
||||
def write(self, data):
|
||||
if self.capture:
|
||||
self.capture.write(data)
|
||||
else:
|
||||
for stream in self.streams:
|
||||
# Naive hole punching to create sparse files
|
||||
if stream is self.backing and util.is_zero(data):
|
||||
stream.seek(len(data), os.SEEK_CUR)
|
||||
else:
|
||||
stream.write(data)
|
||||
|
||||
return len(data)
|
||||
|
||||
def flush(self):
|
||||
for stream in self.streams:
|
||||
stream.flush()
|
||||
|
||||
def tell(self):
|
||||
if self.backing is None:
|
||||
# Fake non-existance
|
||||
raise AttributeError('tell is not supported')
|
||||
|
||||
capture_len = self.capture.tell() if self.capture else 0
|
||||
return self.backing.tell() + capture_len
|
||||
|
||||
def start_capture(self):
|
||||
if self.capture is not None:
|
||||
raise RuntimeError('Capture already started')
|
||||
|
||||
self.capture = _MemoryFile()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _finish_capture(self):
|
||||
if not self.capture:
|
||||
raise RuntimeError('No capture started')
|
||||
|
||||
yield self.capture
|
||||
|
||||
for stream in self.streams:
|
||||
stream.write(self.capture.getbuffer())
|
||||
|
||||
self.capture.close()
|
||||
self.capture = None
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open_signing_wrapper(f, privkey, passphrase, cert):
|
||||
'''
|
||||
Create a file-like wrapper around an existing file object that performs CMS
|
||||
signing as data is being written.
|
||||
'''
|
||||
|
||||
with openssl.inject_passphrase(passphrase):
|
||||
session_kwargs = {}
|
||||
if os.name != 'nt':
|
||||
# We don't want the controlling terminal to interrupt openssl on
|
||||
# ^C or ^\. That'll cause _TeeFileDescriptor's writes to the stdin
|
||||
# pipe to fail, and certain classes, like ZipFile, will write to
|
||||
# the fd in their __exit__ methods. This causes a BrokenPipeError
|
||||
# to be raised while the existing KeyboardInterrupt is being
|
||||
# propagated up. We'll handling killing openssl ourselves.
|
||||
session_kwargs['start_new_session'] = True
|
||||
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
'openssl',
|
||||
'cms',
|
||||
'-sign',
|
||||
'-binary',
|
||||
'-outform', 'DER',
|
||||
'-inkey', privkey,
|
||||
'-signer', cert,
|
||||
# Mimic signapk behavior by excluding signed attributes
|
||||
'-noattr',
|
||||
'-nosmimecap',
|
||||
],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
**session_kwargs,
|
||||
)
|
||||
|
||||
try:
|
||||
wrapper = _TeeFileDescriptor((f, process.stdin), file_index=0)
|
||||
yield wrapper
|
||||
|
||||
with wrapper._finish_capture() as f_buffer:
|
||||
# Save a copy of the zip central directory
|
||||
f_buffer.seek(0)
|
||||
footer = f_buffer.read()
|
||||
|
||||
# Delete the archive comment size field
|
||||
if len(footer) < 2:
|
||||
raise Exception('zip central directory is too small')
|
||||
elif footer[-2:] != b'\x00\x00':
|
||||
raise Exception('zip has unexpected archive comment')
|
||||
|
||||
f_buffer.seek(-2, os.SEEK_CUR)
|
||||
f_buffer.truncate(f_buffer.tell())
|
||||
|
||||
process.stdin.close()
|
||||
signature = process.stdout.read()
|
||||
except BaseException:
|
||||
process.kill()
|
||||
raise
|
||||
finally:
|
||||
process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
raise Exception(f'openssl exited with status: {process.returncode}')
|
||||
|
||||
# Double check that the EOCD magic is where it should be when there is no
|
||||
# archive comment
|
||||
if footer[-22:-18] != zipfile.stringEndArchive:
|
||||
raise Exception('EOCD magic not found')
|
||||
|
||||
# Build a new archive comment that contains the signature
|
||||
with io.BytesIO() as comment:
|
||||
message = b'signed by avbroot\0'
|
||||
comment.write(message)
|
||||
comment.write(signature)
|
||||
|
||||
comment_size = comment.tell() + 6
|
||||
|
||||
if comment_size > 0xffff:
|
||||
raise Exception('Archive comment with signature is too large')
|
||||
|
||||
comment.write(struct.pack(
|
||||
'<HHH',
|
||||
# Absolute value of the offset of the signature from the end of the
|
||||
# archive comment
|
||||
comment_size - len(message),
|
||||
0xffff,
|
||||
comment_size,
|
||||
))
|
||||
|
||||
# Verify that we won't be producing a duplicate EOCD magic
|
||||
if zipfile.stringEndArchive in comment.getbuffer():
|
||||
raise Exception('Archive comment contains EOCD magic')
|
||||
|
||||
# Write comment size to output file (which was removed before)
|
||||
f.write(struct.pack('<H', comment_size))
|
||||
|
||||
# Write comment to output file
|
||||
f.write(comment.getbuffer())
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def match_android_zip64_limit():
|
||||
'''
|
||||
Python's ZipFile implementation uses zip64 when the size of an entry is >
|
||||
0x7fffffff. However, Android's libarchive behavior is incorrect [1] and
|
||||
treats the data descriptor size fields as 32-bit unless the compressed or
|
||||
uncompressed size in the central directory is >= 0xffffffff. This causes
|
||||
files containing entries with sizes in [2 GiB, 4 GiB - 2] to fail to flash
|
||||
in Android's recovery environment. Work around this by changing ZipFile's
|
||||
threshold to match Android's.
|
||||
|
||||
[1] https://cs.android.com/android/platform/superproject/+/android-13.0.0_r18:system/libziparchive/zip_archive.cc;l=692
|
||||
'''
|
||||
|
||||
# Because Python uses > and Android uses >= 0xffffffff
|
||||
with unittest.mock.patch('zipfile.ZIP64_LIMIT', 0xfffffffe):
|
||||
yield
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If you change this file,
|
||||
// Please update ota_metadata_pb2.py by executing
|
||||
// protoc ota_metadata.proto --python_out
|
||||
// $ANDROID_BUILD_TOP/build/tools/releasetools
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package build.tools.releasetools;
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
option java_package = "android.ota";
|
||||
option java_outer_classname = "OtaPackageMetadata";
|
||||
|
||||
// The build information of a particular partition on the device.
|
||||
message PartitionState {
|
||||
string partition_name = 1;
|
||||
repeated string device = 2;
|
||||
repeated string build = 3;
|
||||
// The version string of the partition. It's usually timestamp if present.
|
||||
// One known exception is the boot image, who uses the kmi version, e.g.
|
||||
// 5.4.42-android12-0
|
||||
string version = 4;
|
||||
|
||||
// TODO(xunchang), revisit other necessary fields, e.g. security_patch_level.
|
||||
}
|
||||
|
||||
// The build information on the device. The bytes of the running images are thus
|
||||
// inferred from the device state. For more information of the meaning of each
|
||||
// subfield, check
|
||||
// https://source.android.com/compatibility/android-cdd#3_2_2_build_parameters
|
||||
message DeviceState {
|
||||
// device name. i.e. ro.product.device; if the field has multiple values, it
|
||||
// means the ota package supports multiple devices. This usually happens when
|
||||
// we use the same image to support multiple skus.
|
||||
repeated string device = 1;
|
||||
// device fingerprint. Up to R build, the value reads from
|
||||
// ro.build.fingerprint.
|
||||
repeated string build = 2;
|
||||
// A value that specify a version of the android build.
|
||||
string build_incremental = 3;
|
||||
// The timestamp when the build is generated.
|
||||
int64 timestamp = 4;
|
||||
// The version of the currently-executing Android system.
|
||||
string sdk_level = 5;
|
||||
// A value indicating the security patch level of a build.
|
||||
string security_patch_level = 6;
|
||||
|
||||
// The detailed state of each partition. For partial updates or devices with
|
||||
// mixed build of partitions, some of the above fields may left empty. And the
|
||||
// client will rely on the information of specific partitions to target the
|
||||
// update.
|
||||
repeated PartitionState partition_state = 7;
|
||||
}
|
||||
|
||||
message ApexInfo {
|
||||
string package_name = 1;
|
||||
int64 version = 2;
|
||||
bool is_compressed = 3;
|
||||
int64 decompressed_size = 4;
|
||||
// Used in OTA
|
||||
int64 source_version = 5;
|
||||
}
|
||||
|
||||
// Just a container to hold repeated apex_info, so that we can easily serialize
|
||||
// a list of apex_info to string.
|
||||
message ApexMetadata {
|
||||
repeated ApexInfo apex_info = 1;
|
||||
}
|
||||
|
||||
// The metadata of an OTA package. It contains the information of the package
|
||||
// and prerequisite to install the update correctly.
|
||||
message OtaMetadata {
|
||||
enum OtaType {
|
||||
UNKNOWN = 0;
|
||||
AB = 1;
|
||||
BLOCK = 2;
|
||||
BRICK = 3;
|
||||
};
|
||||
OtaType type = 1;
|
||||
// True if we need to wipe after the update.
|
||||
bool wipe = 2;
|
||||
// True if the timestamp of the post build is older than the pre build.
|
||||
bool downgrade = 3;
|
||||
// A map of name:content of property files, e.g. ota-property-files.
|
||||
map<string, string> property_files = 4;
|
||||
|
||||
// The required device state in order to install the package.
|
||||
DeviceState precondition = 5;
|
||||
// The expected device state after the update.
|
||||
DeviceState postcondition = 6;
|
||||
|
||||
// True if the ota that updates a device to support dynamic partitions, where
|
||||
// the source build doesn't support it.
|
||||
bool retrofit_dynamic_partitions = 7;
|
||||
// The required size of the cache partition, only valid for non-A/B update.
|
||||
int64 required_cache = 8;
|
||||
|
||||
// True iff security patch level downgrade is permitted on this OTA.
|
||||
bool spl_downgrade = 9;
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
//
|
||||
// Copyright (C) 2010 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// Update file format: An update file contains all the operations needed
|
||||
// to update a system to a specific version. It can be a full payload which
|
||||
// can update from any version, or a delta payload which can only update
|
||||
// from a specific version.
|
||||
// The update format is represented by this struct pseudocode:
|
||||
// struct delta_update_file {
|
||||
// char magic[4] = "CrAU";
|
||||
// uint64 file_format_version; // payload major version
|
||||
// uint64 manifest_size; // Size of protobuf DeltaArchiveManifest
|
||||
//
|
||||
// // Only present if format_version >= 2:
|
||||
// uint32 metadata_signature_size;
|
||||
//
|
||||
// // The DeltaArchiveManifest protobuf serialized, not compressed.
|
||||
// char manifest[manifest_size];
|
||||
//
|
||||
// // The signature of the metadata (from the beginning of the payload up to
|
||||
// // this location, not including the signature itself). This is a serialized
|
||||
// // Signatures message.
|
||||
// char metadata_signature_message[metadata_signature_size];
|
||||
//
|
||||
// // Data blobs for files, no specific format. The specific offset
|
||||
// // and length of each data blob is recorded in the DeltaArchiveManifest.
|
||||
// struct {
|
||||
// char data[];
|
||||
// } blobs[];
|
||||
//
|
||||
// // The signature of the entire payload, everything up to this location,
|
||||
// // except that metadata_signature_message is skipped to simplify signing
|
||||
// // process. These two are not signed:
|
||||
// uint64 payload_signatures_message_size;
|
||||
// // This is a serialized Signatures message.
|
||||
// char payload_signatures_message[payload_signatures_message_size];
|
||||
//
|
||||
// };
|
||||
|
||||
// The DeltaArchiveManifest protobuf is an ordered list of InstallOperation
|
||||
// objects. These objects are stored in a linear array in the
|
||||
// DeltaArchiveManifest. Each operation is applied in order by the client.
|
||||
|
||||
// The DeltaArchiveManifest also contains the initial and final
|
||||
// checksums for the device.
|
||||
|
||||
// The client will perform each InstallOperation in order, beginning even
|
||||
// before the entire delta file is downloaded (but after at least the
|
||||
// protobuf is downloaded). The types of operations are explained:
|
||||
// - REPLACE: Replace the dst_extents on the drive with the attached data,
|
||||
// zero padding out to block size.
|
||||
// - REPLACE_BZ: bzip2-uncompress the attached data and write it into
|
||||
// dst_extents on the drive, zero padding to block size.
|
||||
// - MOVE: Copy the data in src_extents to dst_extents. Extents may overlap,
|
||||
// so it may be desirable to read all src_extents data into memory before
|
||||
// writing it out. (deprecated)
|
||||
// - SOURCE_COPY: Copy the data in src_extents in the old partition to
|
||||
// dst_extents in the new partition. There's no overlapping of data because
|
||||
// the extents are in different partitions.
|
||||
// - BSDIFF: Read src_length bytes from src_extents into memory, perform
|
||||
// bspatch with attached data, write new data to dst_extents, zero padding
|
||||
// to block size. (deprecated)
|
||||
// - SOURCE_BSDIFF: Read the data in src_extents in the old partition, perform
|
||||
// bspatch with the attached data and write the new data to dst_extents in the
|
||||
// new partition.
|
||||
// - ZERO: Write zeros to the destination dst_extents.
|
||||
// - DISCARD: Discard the destination dst_extents blocks on the physical medium.
|
||||
// the data read from those blocks is undefined.
|
||||
// - REPLACE_XZ: Replace the dst_extents with the contents of the attached
|
||||
// xz file after decompression. The xz file should only use crc32 or no crc at
|
||||
// all to be compatible with xz-embedded.
|
||||
// - PUFFDIFF: Read the data in src_extents in the old partition, perform
|
||||
// puffpatch with the attached data and write the new data to dst_extents in
|
||||
// the new partition.
|
||||
//
|
||||
// The operations allowed in the payload (supported by the client) depend on the
|
||||
// major and minor version. See InstallOperation.Type below for details.
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package chromeos_update_engine;
|
||||
|
||||
// Data is packed into blocks on disk, always starting from the beginning
|
||||
// of the block. If a file's data is too large for one block, it overflows
|
||||
// into another block, which may or may not be the following block on the
|
||||
// physical partition. An ordered list of extents is another
|
||||
// representation of an ordered list of blocks. For example, a file stored
|
||||
// in blocks 9, 10, 11, 2, 18, 12 (in that order) would be stored in
|
||||
// extents { {9, 3}, {2, 1}, {18, 1}, {12, 1} } (in that order).
|
||||
// In general, files are stored sequentially on disk, so it's more efficient
|
||||
// to use extents to encode the block lists (this is effectively
|
||||
// run-length encoding).
|
||||
// A sentinel value (kuint64max) as the start block denotes a sparse-hole
|
||||
// in a file whose block-length is specified by num_blocks.
|
||||
|
||||
message Extent {
|
||||
optional uint64 start_block = 1;
|
||||
optional uint64 num_blocks = 2;
|
||||
}
|
||||
|
||||
// Signatures: Updates may be signed by the OS vendor. The client verifies
|
||||
// an update's signature by hashing the entire download. The section of the
|
||||
// download that contains the signature is at the end of the file, so when
|
||||
// signing a file, only the part up to the signature part is signed.
|
||||
// Then, the client looks inside the download's Signatures message for a
|
||||
// Signature message that it knows how to handle. Generally, a client will
|
||||
// only know how to handle one type of signature, but an update may contain
|
||||
// many signatures to support many different types of client. Then client
|
||||
// selects a Signature message and uses that, along with a known public key,
|
||||
// to verify the download. The public key is expected to be part of the
|
||||
// client.
|
||||
|
||||
message Signatures {
|
||||
message Signature {
|
||||
optional uint32 version = 1 [deprecated = true];
|
||||
optional bytes data = 2;
|
||||
|
||||
// The DER encoded signature size of EC keys is nondeterministic for
|
||||
// different input of sha256 hash. However, we need the size of the
|
||||
// serialized signatures protobuf string to be fixed before signing;
|
||||
// because this size is part of the content to be signed. Therefore, we
|
||||
// always pad the signature data to the maximum possible signature size of
|
||||
// a given key. And the payload verifier will truncate the signature to
|
||||
// its correct size based on the value of |unpadded_signature_size|.
|
||||
optional fixed32 unpadded_signature_size = 3;
|
||||
}
|
||||
repeated Signature signatures = 1;
|
||||
}
|
||||
|
||||
message PartitionInfo {
|
||||
optional uint64 size = 1;
|
||||
optional bytes hash = 2;
|
||||
}
|
||||
|
||||
message InstallOperation {
|
||||
enum Type {
|
||||
REPLACE = 0; // Replace destination extents w/ attached data.
|
||||
REPLACE_BZ = 1; // Replace destination extents w/ attached bzipped data.
|
||||
MOVE = 2 [deprecated = true]; // Move source extents to target extents.
|
||||
BSDIFF = 3 [deprecated = true]; // The data is a bsdiff binary diff.
|
||||
|
||||
// On minor version 2 or newer, these operations are supported:
|
||||
SOURCE_COPY = 4; // Copy from source to target partition
|
||||
SOURCE_BSDIFF = 5; // Like BSDIFF, but read from source partition
|
||||
|
||||
// On minor version 3 or newer and on major version 2 or newer, these
|
||||
// operations are supported:
|
||||
REPLACE_XZ = 8; // Replace destination extents w/ attached xz data.
|
||||
|
||||
// On minor version 4 or newer, these operations are supported:
|
||||
ZERO = 6; // Write zeros in the destination.
|
||||
DISCARD = 7; // Discard the destination blocks, reading as undefined.
|
||||
BROTLI_BSDIFF = 10; // Like SOURCE_BSDIFF, but compressed with brotli.
|
||||
|
||||
// On minor version 5 or newer, these operations are supported:
|
||||
PUFFDIFF = 9; // The data is in puffdiff format.
|
||||
|
||||
// On minor version 8 or newer, these operations are supported:
|
||||
ZUCCHINI = 11;
|
||||
|
||||
// On minor version 9 or newer, these operations are supported:
|
||||
LZ4DIFF_BSDIFF = 12;
|
||||
LZ4DIFF_PUFFDIFF = 13;
|
||||
}
|
||||
required Type type = 1;
|
||||
|
||||
// Only minor version 6 or newer support 64 bits |data_offset| and
|
||||
// |data_length|, older client will read them as uint32.
|
||||
// The offset into the delta file (after the protobuf)
|
||||
// where the data (if any) is stored
|
||||
optional uint64 data_offset = 2;
|
||||
// The length of the data in the delta file
|
||||
optional uint64 data_length = 3;
|
||||
|
||||
// Ordered list of extents that are read from (if any) and written to.
|
||||
repeated Extent src_extents = 4;
|
||||
// Byte length of src, equal to the number of blocks in src_extents *
|
||||
// block_size. It is used for BSDIFF and SOURCE_BSDIFF, because we need to
|
||||
// pass that external program the number of bytes to read from the blocks we
|
||||
// pass it. This is not used in any other operation.
|
||||
optional uint64 src_length = 5;
|
||||
|
||||
repeated Extent dst_extents = 6;
|
||||
// Byte length of dst, equal to the number of blocks in dst_extents *
|
||||
// block_size. Used for BSDIFF and SOURCE_BSDIFF, but not in any other
|
||||
// operation.
|
||||
optional uint64 dst_length = 7;
|
||||
|
||||
// Optional SHA 256 hash of the blob associated with this operation.
|
||||
// This is used as a primary validation for http-based downloads and
|
||||
// as a defense-in-depth validation for https-based downloads. If
|
||||
// the operation doesn't refer to any blob, this field will have
|
||||
// zero bytes.
|
||||
optional bytes data_sha256_hash = 8;
|
||||
|
||||
// Indicates the SHA 256 hash of the source data referenced in src_extents at
|
||||
// the time of applying the operation. If present, the update_engine daemon
|
||||
// MUST read and verify the source data before applying the operation.
|
||||
optional bytes src_sha256_hash = 9;
|
||||
}
|
||||
|
||||
// Hints to VAB snapshot to skip writing some blocks if these blocks are
|
||||
// identical to the ones on the source image. The src & dst extents for each
|
||||
// CowMergeOperation should be contiguous, and they're a subset of an OTA
|
||||
// InstallOperation.
|
||||
// During merge time, we need to follow the pre-computed sequence to avoid
|
||||
// read after write, similar to the inplace update schema.
|
||||
message CowMergeOperation {
|
||||
enum Type {
|
||||
COW_COPY = 0; // identical blocks
|
||||
COW_XOR = 1; // used when src/dst blocks are highly similar
|
||||
COW_REPLACE = 2; // Raw replace operation
|
||||
}
|
||||
optional Type type = 1;
|
||||
|
||||
optional Extent src_extent = 2;
|
||||
optional Extent dst_extent = 3;
|
||||
// For COW_XOR, source location might be unaligned, so this field is in range
|
||||
// [0, block_size), representing how much should the src_extent shift toward
|
||||
// larger block number. If this field is non-zero, then src_extent will
|
||||
// include 1 extra block in the end, as the merge op actually references the
|
||||
// first |src_offset| bytes of that extra block. For example, if |dst_extent|
|
||||
// is [10, 15], |src_offset| is 500, then src_extent might look like [25, 31].
|
||||
// Note that |src_extent| contains 1 extra block than the |dst_extent|.
|
||||
optional uint32 src_offset = 4;
|
||||
}
|
||||
|
||||
// Describes the update to apply to a single partition.
|
||||
message PartitionUpdate {
|
||||
// A platform-specific name to identify the partition set being updated. For
|
||||
// example, in Chrome OS this could be "ROOT" or "KERNEL".
|
||||
required string partition_name = 1;
|
||||
|
||||
// Whether this partition carries a filesystem with post-install program that
|
||||
// must be run to finalize the update process. See also |postinstall_path| and
|
||||
// |filesystem_type|.
|
||||
optional bool run_postinstall = 2;
|
||||
|
||||
// The path of the executable program to run during the post-install step,
|
||||
// relative to the root of this filesystem. If not set, the default "postinst"
|
||||
// will be used. This setting is only used when |run_postinstall| is set and
|
||||
// true.
|
||||
optional string postinstall_path = 3;
|
||||
|
||||
// The filesystem type as passed to the mount(2) syscall when mounting the new
|
||||
// filesystem to run the post-install program. If not set, a fixed list of
|
||||
// filesystems will be attempted. This setting is only used if
|
||||
// |run_postinstall| is set and true.
|
||||
optional string filesystem_type = 4;
|
||||
|
||||
// If present, a list of signatures of the new_partition_info.hash signed with
|
||||
// different keys. If the update_engine daemon requires vendor-signed images
|
||||
// and has its public key installed, one of the signatures should be valid
|
||||
// for /postinstall to run.
|
||||
repeated Signatures.Signature new_partition_signature = 5;
|
||||
|
||||
optional PartitionInfo old_partition_info = 6;
|
||||
optional PartitionInfo new_partition_info = 7;
|
||||
|
||||
// The list of operations to be performed to apply this PartitionUpdate. The
|
||||
// associated operation blobs (in operations[i].data_offset, data_length)
|
||||
// should be stored contiguously and in the same order.
|
||||
repeated InstallOperation operations = 8;
|
||||
|
||||
// Whether a failure in the postinstall step for this partition should be
|
||||
// ignored.
|
||||
optional bool postinstall_optional = 9;
|
||||
|
||||
// On minor version 6 or newer, these fields are supported:
|
||||
|
||||
// The extent for data covered by verity hash tree.
|
||||
optional Extent hash_tree_data_extent = 10;
|
||||
|
||||
// The extent to store verity hash tree.
|
||||
optional Extent hash_tree_extent = 11;
|
||||
|
||||
// The hash algorithm used in verity hash tree.
|
||||
optional string hash_tree_algorithm = 12;
|
||||
|
||||
// The salt used for verity hash tree.
|
||||
optional bytes hash_tree_salt = 13;
|
||||
|
||||
// The extent for data covered by FEC.
|
||||
optional Extent fec_data_extent = 14;
|
||||
|
||||
// The extent to store FEC.
|
||||
optional Extent fec_extent = 15;
|
||||
|
||||
// The number of FEC roots.
|
||||
optional uint32 fec_roots = 16 [default = 2];
|
||||
|
||||
// Per-partition version used for downgrade detection, added
|
||||
// as an effort to support partial updates. For most partitions,
|
||||
// this is the build timestamp.
|
||||
optional string version = 17;
|
||||
|
||||
// A sorted list of CowMergeOperation. When writing cow, we can choose to
|
||||
// skip writing the raw bytes for these extents. During snapshot merge, the
|
||||
// bytes will read from the source partitions instead.
|
||||
repeated CowMergeOperation merge_operations = 18;
|
||||
|
||||
// Estimated size for COW image. This is used by libsnapshot
|
||||
// as a hint. If set to 0, libsnapshot should use alternative
|
||||
// methods for estimating size.
|
||||
optional uint64 estimate_cow_size = 19;
|
||||
}
|
||||
|
||||
message DynamicPartitionGroup {
|
||||
// Name of the group.
|
||||
required string name = 1;
|
||||
|
||||
// Maximum size of the group. The sum of sizes of all partitions in the group
|
||||
// must not exceed the maximum size of the group.
|
||||
optional uint64 size = 2;
|
||||
|
||||
// A list of partitions that belong to the group.
|
||||
repeated string partition_names = 3;
|
||||
}
|
||||
|
||||
message VABCFeatureSet {
|
||||
optional bool threaded = 1;
|
||||
optional bool batch_writes = 2;
|
||||
}
|
||||
|
||||
// Metadata related to all dynamic partitions.
|
||||
message DynamicPartitionMetadata {
|
||||
// All updatable groups present in |partitions| of this DeltaArchiveManifest.
|
||||
// - If an updatable group is on the device but not in the manifest, it is
|
||||
// not updated. Hence, the group will not be resized, and partitions cannot
|
||||
// be added to or removed from the group.
|
||||
// - If an updatable group is in the manifest but not on the device, the group
|
||||
// is added to the device.
|
||||
repeated DynamicPartitionGroup groups = 1;
|
||||
|
||||
// Whether dynamic partitions have snapshots during the update. If this is
|
||||
// set to true, the update_engine daemon creates snapshots for all dynamic
|
||||
// partitions if possible. If this is unset, the update_engine daemon MUST
|
||||
// NOT create snapshots for dynamic partitions.
|
||||
optional bool snapshot_enabled = 2;
|
||||
|
||||
// If this is set to false, update_engine should not use VABC regardless. If
|
||||
// this is set to true, update_engine may choose to use VABC if device
|
||||
// supports it, but not guaranteed.
|
||||
// VABC stands for Virtual AB Compression
|
||||
optional bool vabc_enabled = 3;
|
||||
|
||||
// The compression algorithm used by VABC. Available ones are "gz", "brotli".
|
||||
// See system/core/fs_mgr/libsnapshot/cow_writer.cpp for available options,
|
||||
// as this parameter is ultimated forwarded to libsnapshot's CowWriter
|
||||
optional string vabc_compression_param = 4;
|
||||
|
||||
// COW version used by VABC. The represents the major version in the COW
|
||||
// header
|
||||
optional uint32 cow_version = 5;
|
||||
|
||||
// A collection of knobs to tune Virtual AB Compression
|
||||
optional VABCFeatureSet vabc_feature_set = 6;
|
||||
}
|
||||
|
||||
// Definition has been duplicated from
|
||||
// $ANDROID_BUILD_TOP/build/tools/releasetools/ota_metadata.proto. Keep in sync.
|
||||
message ApexInfo {
|
||||
optional string package_name = 1;
|
||||
optional int64 version = 2;
|
||||
optional bool is_compressed = 3;
|
||||
optional int64 decompressed_size = 4;
|
||||
}
|
||||
|
||||
// Definition has been duplicated from
|
||||
// $ANDROID_BUILD_TOP/build/tools/releasetools/ota_metadata.proto. Keep in sync.
|
||||
message ApexMetadata {
|
||||
repeated ApexInfo apex_info = 1;
|
||||
}
|
||||
|
||||
message DeltaArchiveManifest {
|
||||
// Only present in major version = 1. List of install operations for the
|
||||
// kernel and rootfs partitions. For major version = 2 see the |partitions|
|
||||
// field.
|
||||
reserved 1, 2;
|
||||
|
||||
// (At time of writing) usually 4096
|
||||
optional uint32 block_size = 3 [default = 4096];
|
||||
|
||||
// If signatures are present, the offset into the blobs, generally
|
||||
// tacked onto the end of the file, and the length. We use an offset
|
||||
// rather than a bool to allow for more flexibility in future file formats.
|
||||
// If either is absent, it means signatures aren't supported in this
|
||||
// file.
|
||||
optional uint64 signatures_offset = 4;
|
||||
optional uint64 signatures_size = 5;
|
||||
|
||||
// Fields deprecated in major version 2.
|
||||
reserved 6,7,8,9,10,11;
|
||||
|
||||
// The minor version, also referred as "delta version", of the payload.
|
||||
// Minor version 0 is full payload, everything else is delta payload.
|
||||
optional uint32 minor_version = 12 [default = 0];
|
||||
|
||||
// Only present in major version >= 2. List of partitions that will be
|
||||
// updated, in the order they will be updated. This field replaces the
|
||||
// |install_operations|, |kernel_install_operations| and the
|
||||
// |{old,new}_{kernel,rootfs}_info| fields used in major version = 1. This
|
||||
// array can have more than two partitions if needed, and they are identified
|
||||
// by the partition name.
|
||||
repeated PartitionUpdate partitions = 13;
|
||||
|
||||
// The maximum timestamp of the OS allowed to apply this payload.
|
||||
// Can be used to prevent downgrading the OS.
|
||||
optional int64 max_timestamp = 14;
|
||||
|
||||
// Metadata related to all dynamic partitions.
|
||||
optional DynamicPartitionMetadata dynamic_partition_metadata = 15;
|
||||
|
||||
// If the payload only updates a subset of partitions on the device.
|
||||
optional bool partial_update = 16;
|
||||
|
||||
// Information on compressed APEX to figure out how much space is required for
|
||||
// their decompression
|
||||
repeated ApexInfo apex_info = 17;
|
||||
|
||||
// Security patch level of the device, usually in the format of
|
||||
// yyyy-mm-dd
|
||||
optional string security_patch_level = 18;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, IsTerminal},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
Avb(avb::AvbCli),
|
||||
Boot(boot::BootCli),
|
||||
Completion(completion::CompletionCli),
|
||||
Cpio(cpio::CpioCli),
|
||||
Fec(fec::FecCli),
|
||||
HashTree(hashtree::HashTreeCli),
|
||||
Key(key::KeyCli),
|
||||
Ota(ota::OtaCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
Extract(ota::ExtractCli),
|
||||
/// (Deprecated: Use `avbroot boot magisk-info` instead.)
|
||||
MagiskInfo(boot::MagiskInfoCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn as_level(self) -> Level {
|
||||
match self {
|
||||
Self::Trace => Level::TRACE,
|
||||
Self::Debug => Level::DEBUG,
|
||||
Self::Info => Level::INFO,
|
||||
Self::Warn => Level::WARN,
|
||||
Self::Error => Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LogLevel {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum LogFormat {
|
||||
Short,
|
||||
Medium,
|
||||
Long,
|
||||
}
|
||||
|
||||
impl Default for LogFormat {
|
||||
fn default() -> Self {
|
||||
Self::Short
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
|
||||
pub log_level: LogLevel,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ShortUptime {
|
||||
epoch: Instant,
|
||||
}
|
||||
|
||||
impl Default for ShortUptime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
epoch: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatTime for ShortUptime {
|
||||
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
|
||||
let e = self.epoch.elapsed();
|
||||
write!(w, "{:3}.{:03}s", e.as_secs(), e.subsec_millis())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
|
||||
let builder = tracing_subscriber::fmt()
|
||||
.with_writer(io::stderr)
|
||||
.with_ansi(io::stderr().is_terminal())
|
||||
.with_max_level(log_level.as_level());
|
||||
|
||||
match log_format {
|
||||
LogFormat::Short => {
|
||||
let format = tracing_subscriber::fmt::format()
|
||||
.with_timer(ShortUptime::default())
|
||||
.with_target(false);
|
||||
|
||||
builder.event_format(format).init();
|
||||
}
|
||||
LogFormat::Medium => {
|
||||
builder.with_timer(ShortUptime::default()).init();
|
||||
}
|
||||
LogFormat::Long => {
|
||||
builder.pretty().init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
init_logging(cli.log_level, cli.log_format);
|
||||
logging_initialized.store(true, Ordering::SeqCst);
|
||||
|
||||
debug!(?cli);
|
||||
|
||||
match cli.command {
|
||||
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
|
||||
Command::Boot(c) => boot::boot_main(&c),
|
||||
Command::Completion(c) => completion::completion_main(&c),
|
||||
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
|
||||
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
|
||||
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
// Deprecated aliases.
|
||||
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
|
||||
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
|
||||
Command::MagiskInfo(c) => boot::magisk_info_subcommand(&c),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,915 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use cap_std::{
|
||||
ambient_authority,
|
||||
fs::{Dir, OpenOptions},
|
||||
};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use rsa::RsaPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug_span, info, warn, Span};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile, Reopen},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
struct AvbInfo {
|
||||
header: Header,
|
||||
footer: Option<Footer>,
|
||||
image_size: u64,
|
||||
}
|
||||
|
||||
fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
|
||||
let file = File::open(path)
|
||||
.with_context(|| format!("Failed to open AVB image for reading: {path:?}"))?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let (header, footer, image_size) = avb::load_image(&mut reader)
|
||||
.with_context(|| format!("Failed to load AVB image: {path:?}"))?;
|
||||
|
||||
let info = AvbInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
};
|
||||
|
||||
Ok((info, reader))
|
||||
}
|
||||
|
||||
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo) -> Result<()> {
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
if let Some(f) = &mut info.footer {
|
||||
avb::write_appended_image(&mut writer, &info.header, f, info.image_size)
|
||||
.context("Failed to write appended AVB image")?;
|
||||
} else {
|
||||
avb::write_root_image(&mut writer, &info.header, 4096)
|
||||
.context("Failed to write root AVB image")?;
|
||||
}
|
||||
|
||||
writer.flush().context("Failed to flush writes")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read AVB information from TOML file.
|
||||
fn read_info(path: &Path) -> Result<AvbInfo> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read AVB info TOML: {path:?}"))?;
|
||||
let info = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse AVB info TOML: {path:?}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Write AVB information to TOML file.
|
||||
fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(info)
|
||||
.with_context(|| format!("Failed to serialize AVB info TOML: {path:?}"))?;
|
||||
fs::write(path, data).with_context(|| format!("Failed to write AVB info TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Packing with insecure algorithms is intentionally not supported, so promote
|
||||
/// to a secure algorithm if needed.
|
||||
fn promote_insecure_hash_algorithm(algorithm: &mut String) {
|
||||
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
|
||||
const NEW_ALGORITHM: &str = "sha256";
|
||||
|
||||
if INSECURE_ALGORITHMS.contains(&algorithm.as_str()) {
|
||||
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
|
||||
NEW_ALGORITHM.clone_into(algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
|
||||
/// both readable and writable.
|
||||
fn write_raw(
|
||||
path: &Path,
|
||||
reader: &mut BufReader<File>,
|
||||
size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open raw image for writing: {path:?}"))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
reader
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
stream::copy_n(reader, &mut writer, size, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy raw image: {path:?}"))?;
|
||||
|
||||
let file = writer
|
||||
.into_inner()
|
||||
.with_context(|| format!("Failed to flush writes: {path:?}"))?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Copy a raw image into `path` and verify the descriptor. If corruption is
|
||||
/// detected and FEC data is available, then a repair is attempted.
|
||||
fn write_raw_and_verify(
|
||||
path: &Path,
|
||||
reader: &mut BufReader<File>,
|
||||
info: &AvbInfo,
|
||||
ignore_invalid: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
let f = info.footer.as_ref().expect("Not an appended image");
|
||||
|
||||
let descriptor = info.header.appended_descriptor()?;
|
||||
|
||||
// The hash tree and FEC need to be present for verification. We don't
|
||||
// verify against the original file because a repair would require writes.
|
||||
let copy_size = match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => d.image_size + d.tree_size + d.fec_size,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
};
|
||||
|
||||
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
|
||||
|
||||
let result = verify_and_repair(None, raw_file.reopen()?, descriptor, true, cancel_signal);
|
||||
|
||||
// Chop off the old hash tree and FEC data.
|
||||
raw_file.set_len(f.original_image_size)?;
|
||||
|
||||
if let Err(e) = result {
|
||||
if ignore_invalid {
|
||||
warn!("{e:?}");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(raw_file)
|
||||
}
|
||||
|
||||
/// Copy a raw image into `path` and update the appended descriptor with the new
|
||||
/// digests, offsets, and sizes.
|
||||
fn write_raw_and_update(
|
||||
path: &Path,
|
||||
reader: &mut BufReader<File>,
|
||||
info: &mut AvbInfo,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
assert!(info.footer.is_some(), "Not an appended image");
|
||||
|
||||
let image_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.context("Failed to get input data size")?;
|
||||
|
||||
let mut raw_file = write_raw(path, reader, image_size, cancel_signal)?;
|
||||
|
||||
match info.header.appended_descriptor_mut()? {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
|
||||
d.image_size = image_size;
|
||||
d.update(&raw_file, &raw_file, None, cancel_signal)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
}
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
|
||||
d.image_size = image_size;
|
||||
raw_file.rewind()?;
|
||||
d.update(&mut raw_file, cancel_signal)
|
||||
.context("Failed to update hash descriptor")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(raw_file)
|
||||
}
|
||||
|
||||
/// Compute the kernel command line arguments to allow the kernel to set up the
|
||||
/// dm-verity block device without dm-init or userspace helpers. This is handled
|
||||
/// by init/do_mounts_dm.c in older Pixel devices (and ChromiumOS).
|
||||
fn compute_dm_verity_cmdline(descriptor: &HashTreeDescriptor) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
// Number of device mapper devices.
|
||||
result.push_str("dm=\"1");
|
||||
// Block device name.
|
||||
result.push_str(" vroot");
|
||||
// Block device UUID.
|
||||
result.push_str(" none");
|
||||
// Block device write mode.
|
||||
result.push_str(" ro");
|
||||
// Number of device mapper targets.
|
||||
result.push_str(" 1,");
|
||||
// Starting sector.
|
||||
result.push('0');
|
||||
// Sector count.
|
||||
write!(&mut result, " {}", descriptor.image_size / 512).unwrap();
|
||||
// dm-verity version.
|
||||
write!(&mut result, " verity {}", descriptor.dm_verity_version).unwrap();
|
||||
// Data block device (replaced by bootloader at runtime).
|
||||
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
|
||||
// Hash block device (replaced by bootloader at runtime).
|
||||
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
|
||||
// Data block size.
|
||||
write!(&mut result, " {}", descriptor.data_block_size).unwrap();
|
||||
// Hash block size.
|
||||
write!(&mut result, " {}", descriptor.hash_block_size).unwrap();
|
||||
// Number of data blocks.
|
||||
write!(
|
||||
&mut result,
|
||||
" {}",
|
||||
descriptor.image_size / u64::from(descriptor.data_block_size),
|
||||
)
|
||||
.unwrap();
|
||||
// Hash starting block (in units of the hash block size).
|
||||
write!(
|
||||
&mut result,
|
||||
" {}",
|
||||
descriptor.image_size / u64::from(descriptor.hash_block_size),
|
||||
)
|
||||
.unwrap();
|
||||
// Hash algorithm.
|
||||
write!(&mut result, " {}", descriptor.hash_algorithm).unwrap();
|
||||
// Root digest.
|
||||
write!(&mut result, " {}", &hex::encode(&descriptor.root_digest)).unwrap();
|
||||
// Salt.
|
||||
write!(&mut result, " {}", &hex::encode(&descriptor.salt)).unwrap();
|
||||
|
||||
// Number of optional arguments.
|
||||
let num_optional_args = if descriptor.fec_num_roots != 0 { 10 } else { 2 }
|
||||
+ u8::from(descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0);
|
||||
write!(&mut result, " {num_optional_args}").unwrap();
|
||||
|
||||
if descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0 {
|
||||
// [n + 1] Only check blocks once instead of on each access.
|
||||
result.push_str(" check_at_most_once");
|
||||
}
|
||||
|
||||
// [0] Corruption handling mode (replaced by bootloader at runtime).
|
||||
result.push_str(" $(ANDROID_VERITY_MODE)");
|
||||
// [1] Force return zeros and skip validation for blocks expected to contain
|
||||
// only zeros.
|
||||
result.push_str(" ignore_zero_blocks");
|
||||
|
||||
if descriptor.fec_num_roots != 0 {
|
||||
// [2-3] Enable FEC (replaced by bootloader at runtime).
|
||||
result.push_str(" use_fec_from_device PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
|
||||
// [4-5] Number of parity bytes per FEC codeword.
|
||||
write!(&mut result, " fec_roots {}", descriptor.fec_num_roots).unwrap();
|
||||
|
||||
let fec_block_offset = descriptor.fec_offset / u64::from(descriptor.data_block_size);
|
||||
|
||||
// [6-7] Number of data blocks covered by FEC.
|
||||
write!(&mut result, " fec_blocks {fec_block_offset}").unwrap();
|
||||
// [8-9] Starting block (in data block size units) of FEC.
|
||||
write!(&mut result, " fec_start {fec_block_offset}").unwrap();
|
||||
}
|
||||
|
||||
// Root filesystem block device.
|
||||
result.push_str("\" root=/dev/dm-0");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Update the dm-verity kernel command line descriptor to match the hash tree
|
||||
/// descriptor. This is a no-op if there's no matching existing kernel command
|
||||
/// line descriptor to update. Returns whether the descriptor was updated.
|
||||
fn update_dm_verity_cmdline(info: &mut AvbInfo) -> Result<bool> {
|
||||
assert!(info.footer.is_some(), "Not an appended image");
|
||||
|
||||
let new_cmdline = match info.header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => compute_dm_verity_cmdline(d),
|
||||
AppendedDescriptorRef::Hash(_) => return Ok(false),
|
||||
};
|
||||
|
||||
for d in &mut info.header.descriptors {
|
||||
if let Descriptor::KernelCmdline(d) = d {
|
||||
if d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
|
||||
&& d.cmdline.starts_with("dm=")
|
||||
&& d.cmdline != new_cmdline
|
||||
{
|
||||
d.cmdline = new_cmdline;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Sign or clear header signatures based on whether the original header was
|
||||
/// signed. If the original header was signed and is unchanged, then the
|
||||
/// original signature is used as-is. If the force option is specified, then
|
||||
/// the header is signed or cleared based on the presence of the private key
|
||||
/// parameter.
|
||||
fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup) -> Result<()> {
|
||||
enum SignAction {
|
||||
None,
|
||||
Sign,
|
||||
Clear,
|
||||
}
|
||||
|
||||
let originally_signed = !info.header.signature.is_empty();
|
||||
let mut sign_action = if originally_signed && &info.header != orig_header {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
// If the original image was signed, we can preserve the existing
|
||||
// signature since no changes were made. If the original image was not
|
||||
// signed, then there's nothing to do anyway.
|
||||
SignAction::None
|
||||
};
|
||||
|
||||
if key_group.force {
|
||||
sign_action = if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
};
|
||||
}
|
||||
|
||||
match sign_action {
|
||||
SignAction::None => {
|
||||
if originally_signed {
|
||||
info!("Preserving original AVB header signature");
|
||||
} else {
|
||||
info!("Leaving AVB header unsigned");
|
||||
}
|
||||
}
|
||||
SignAction::Sign => {
|
||||
if originally_signed {
|
||||
info!("Replacing AVB header signature");
|
||||
} else {
|
||||
info!("Signing AVB header");
|
||||
}
|
||||
|
||||
let Some(key_path) = &key_group.key else {
|
||||
bail!("Need to sign new AVB header, but no private key was specified");
|
||||
};
|
||||
|
||||
let source = PassphraseSource::new(
|
||||
key_path,
|
||||
key_group.pass_file.as_deref(),
|
||||
key_group.pass_env_var.as_deref(),
|
||||
);
|
||||
let private_key = crypto::read_pem_key_file(key_path, &source)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
|
||||
info.header.set_algo_for_key(&private_key)?;
|
||||
info.header
|
||||
.sign(&private_key)
|
||||
.context("Failed to sign new AVB header")?;
|
||||
}
|
||||
SignAction::Clear => {
|
||||
if originally_signed {
|
||||
info!("Clearing AVB header signature");
|
||||
} else {
|
||||
info!("Leaving AVB header unsigned");
|
||||
}
|
||||
|
||||
info.header.algorithm_type = AlgorithmType::None;
|
||||
info.header.clear_sig();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump AVB information to stdout.
|
||||
fn display_info(display: &DisplayGroup, info: &AvbInfo) {
|
||||
if !display.quiet {
|
||||
println!("{info:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that the partition name won't cause directory traversals.
|
||||
fn ensure_name_is_safe(name: &str) -> Result<()> {
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively verify an image's vbmeta header and all of the chained images.
|
||||
/// `seen` is used to prevent cycles. `descriptors` will contain all of the hash
|
||||
/// and hash tree descriptors that need to be verified.
|
||||
pub fn verify_headers(
|
||||
directory: &Dir,
|
||||
name: &str,
|
||||
expected_key: Option<&RsaPublicKey>,
|
||||
seen: &mut HashSet<String>,
|
||||
descriptors: &mut HashMap<String, Descriptor>,
|
||||
) -> Result<()> {
|
||||
if !seen.insert(name.to_owned()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let raw_reader = directory
|
||||
.open(&path)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (header, _, _) = avb::load_image(BufReader::new(raw_reader))
|
||||
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
|
||||
// Verify the header's signature.
|
||||
let public_key = header
|
||||
.verify()
|
||||
.with_context(|| format!("Failed to verify header signature: {path:?}"))?;
|
||||
|
||||
if let Some(k) = &public_key {
|
||||
let prefix = format!("{name} has a signed vbmeta header");
|
||||
|
||||
if let Some(e) = expected_key {
|
||||
if k == e {
|
||||
info!("{prefix}");
|
||||
} else {
|
||||
bail!("{prefix}, but is signed by an untrusted key");
|
||||
}
|
||||
} else {
|
||||
warn!("{prefix}, but parent does not list a trusted key");
|
||||
}
|
||||
} else {
|
||||
info!("{name} has an unsigned vbmeta header");
|
||||
}
|
||||
|
||||
for descriptor in &header.descriptors {
|
||||
let Some(target_name) = descriptor.partition_name() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match descriptor {
|
||||
avb::Descriptor::HashTree(_) | avb::Descriptor::Hash(_) => {
|
||||
if let Some(prev) = descriptors.get(target_name) {
|
||||
if prev != descriptor {
|
||||
bail!("{name} descriptor does not match previous encounter");
|
||||
}
|
||||
} else {
|
||||
descriptors.insert(target_name.to_owned(), descriptor.clone());
|
||||
}
|
||||
}
|
||||
avb::Descriptor::ChainPartition(d) => {
|
||||
let target_key = avb::decode_public_key(&d.public_key).with_context(|| {
|
||||
format!("Failed to decode chained public key for: {target_name}")
|
||||
})?;
|
||||
|
||||
verify_headers(directory, target_name, Some(&target_key), seen, descriptors)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the descriptor for a file. For hash tree descriptors, if FEC data is
|
||||
/// available and `repair` is true, then attempt to repair data in the event of
|
||||
/// corruption. `file` must be opened as read-write for the repair operation to
|
||||
/// work.
|
||||
fn verify_and_repair(
|
||||
name: Option<&str>,
|
||||
mut file: PSeekFile,
|
||||
descriptor: AppendedDescriptorRef,
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
|
||||
let suffix = match name {
|
||||
Some(n) => format!(" for: {n}"),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
info!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(&file, cancel_signal) {
|
||||
Err(e @ avb::Error::HashTree(_)) if repair => {
|
||||
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warn!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(&file, cancel_signal).map(|_| {
|
||||
info!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
ret => ret,
|
||||
}
|
||||
.with_context(|| format!("Failed to verify hash tree descriptor{suffix}"))?;
|
||||
}
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
info!("Verifying hash descriptor{suffix}");
|
||||
|
||||
file.rewind()?;
|
||||
d.verify(file, cancel_signal)
|
||||
.with_context(|| format!("Failed to verify hash descriptor{suffix}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify hash and hash tree descriptor digests and FEC data against their
|
||||
/// corresponding input files.
|
||||
pub fn verify_descriptors(
|
||||
directory: &Dir,
|
||||
descriptors: &HashMap<String, Descriptor>,
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
descriptors
|
||||
.par_iter()
|
||||
.map(|(name, descriptor)| {
|
||||
let _span = parent_span.enter();
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let file = match directory
|
||||
.open_with(&path, OpenOptions::new().read(true).write(repair))
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
{
|
||||
Ok(f) => f,
|
||||
// Some devices, like bluejay, have vbmeta descriptors that
|
||||
// refer to partitions that exist on the device, but not in the
|
||||
// OTA.
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warn!("Partition image does not exist: {path:?}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?
|
||||
}
|
||||
};
|
||||
|
||||
verify_and_repair(
|
||||
Some(name),
|
||||
file,
|
||||
descriptor.try_into()?,
|
||||
repair,
|
||||
cancel_signal,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (info, mut reader) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
write_info(&cli.output_info, &info)?;
|
||||
|
||||
if info.footer.is_some() {
|
||||
write_raw_and_verify(
|
||||
&cli.output_raw,
|
||||
&mut reader,
|
||||
&info,
|
||||
cli.ignore_invalid,
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut info = read_info(&cli.input_info)?;
|
||||
let orig_header = info.header.clone();
|
||||
|
||||
let file = if info.footer.is_some() {
|
||||
let mut reader = File::open(&cli.input_raw)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| {
|
||||
format!("Failed to open raw image for reading: {:?}", cli.input_raw)
|
||||
})?;
|
||||
|
||||
let file = write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?;
|
||||
|
||||
update_dm_verity_cmdline(&mut info)?;
|
||||
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open output for writing: {:?}", cli.output))?
|
||||
};
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (mut info, mut reader) = read_avb_image(&cli.input)?;
|
||||
let orig_header = info.header.clone();
|
||||
|
||||
let file = if info.footer.is_some() {
|
||||
let file = write_raw_and_verify(&cli.output, &mut reader, &info, false, cancel_signal)?;
|
||||
|
||||
// Write new hash tree and FEC data instead of copying the original.
|
||||
// There could have been errors in the original FEC data itself.
|
||||
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
|
||||
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
|
||||
d.update(&file, &file, None, cancel_signal)?;
|
||||
}
|
||||
|
||||
update_dm_verity_cmdline(&mut info)?;
|
||||
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for writing: {:?}", cli.output))?
|
||||
};
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(cli: &InfoCli) -> Result<()> {
|
||||
let (info, _) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let public_key = if let Some(p) = &cli.public_key {
|
||||
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
|
||||
let key = avb::decode_public_key(&data)
|
||||
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
|
||||
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let authority = ambient_authority();
|
||||
let parent_path = util::parent_path(&cli.input);
|
||||
let directory = Dir::open_ambient_dir(parent_path, authority)
|
||||
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
|
||||
let name = cli
|
||||
.input
|
||||
.file_stem()
|
||||
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
|
||||
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut descriptors = HashMap::<String, Descriptor>::new();
|
||||
|
||||
verify_headers(
|
||||
&directory,
|
||||
name,
|
||||
public_key.as_ref(),
|
||||
&mut seen,
|
||||
&mut descriptors,
|
||||
)?;
|
||||
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
|
||||
|
||||
info!("Successfully verified all vbmeta signatures and hashes");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Pack(c) => pack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Info(c) => info_subcommand(c),
|
||||
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct DisplayGroup {
|
||||
/// Don't print AVB image information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct KeyGroup {
|
||||
/// Path to private key for signing.
|
||||
///
|
||||
/// A private key is needed if packing an image where the original header
|
||||
/// was signed and the header needs to be modified (eg. for a new checksum).
|
||||
/// If the header was originally not signed, then the private key is not
|
||||
/// used, unless --force is specified.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
/// Force signing or clearing signature.
|
||||
///
|
||||
/// If --key is specified, then the output is signed, regardless if the
|
||||
/// original image was signed. Similarly, if --key is not specified, then
|
||||
/// the output is left unsigned, even if the image is unchanged and it's
|
||||
/// possible to preserve an existing signature.
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Unpack an AVB image.
|
||||
///
|
||||
/// This command splits an AVB image into an AVB info TOML file and a raw image.
|
||||
/// For root AVB images (eg. vbmeta), only the info file is created, which will
|
||||
/// contain a header section. For appended AVB images (eg. boot), the raw image
|
||||
/// will be written and the info file will additionally have a footer section.
|
||||
///
|
||||
/// For appended AVB images that contain a hash tree descriptor (eg. dm-verity
|
||||
/// protected system image), FEC data, if available, will be used to repair the
|
||||
/// extracted raw image in the event of data corruption.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output AVB info TOML.
|
||||
///
|
||||
/// The file will always contain a header section, but a footer section will
|
||||
/// only be present if the input is an appended AVB image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
|
||||
output_info: PathBuf,
|
||||
|
||||
/// Path to output raw image.
|
||||
///
|
||||
/// Only appended AVB images will have a raw image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
output_raw: PathBuf,
|
||||
|
||||
/// Ignore invalid digests or FEC data.
|
||||
#[arg(long)]
|
||||
ignore_invalid: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Pack an AVB image.
|
||||
///
|
||||
/// This command creates an AVB image from the AVB info toml file and the raw
|
||||
/// image components. If the info file does not contain a footer, then a root
|
||||
/// image is created. Otherwise, an appended image is created and a raw image
|
||||
/// must be specified.
|
||||
///
|
||||
/// For appended images, the checksums in the header's descriptors and the
|
||||
/// offsets in the footer will be updated. If the header fields are updated and
|
||||
/// the header was originally signed, then the newly created image will also be
|
||||
/// signed. If a signed image is repacked without modification, then the
|
||||
/// original signature is used as-is.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input AVB info TOML.
|
||||
///
|
||||
/// If an appended image is being created, then the hash or hash tree
|
||||
/// descriptor in the header will be updated with the new digest, offset,
|
||||
/// and size field values. There must be exactly one hash or hash tree
|
||||
/// descriptor. The offset and size fields in the footer will be updated as
|
||||
/// well.
|
||||
///
|
||||
/// For root images, all header descriptor fields are left unmodified.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to input raw image.
|
||||
///
|
||||
/// Appended AVB images require a raw image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
input_raw: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Repack an AVB 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 AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Display AVB header and footer information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Verify vbmeta signatures.
|
||||
///
|
||||
/// If the header contains chain descriptors, then those images will be
|
||||
/// recursively verified. For hash tree descriptors, the FEC (forward error
|
||||
/// correction) data will also be verified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to public key in AVB binary format.
|
||||
///
|
||||
/// If this is not specified, the signatures can only be checked for
|
||||
/// validity, not whether they are trusted.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
public_key: Option<PathBuf>,
|
||||
|
||||
/// Repair corrupted files using FEC data if possible.
|
||||
///
|
||||
/// Only images with hash tree descriptors can contain FEC data.
|
||||
#[arg(short, long)]
|
||||
repair: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum AvbCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
#[command(alias = "dump")]
|
||||
Info(InfoCli),
|
||||
Verify(VerifyCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect AVB-protected images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AvbCli {
|
||||
#[command(subcommand)]
|
||||
command: AvbCommand,
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Cursor, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio::CpioReader},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
|
||||
fn read_image(path: &Path) -> Result<BootImage> {
|
||||
let file = File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let reader = BufReader::new(file);
|
||||
let image = BootImage::from_reader(reader)
|
||||
.with_context(|| format!("Failed to read boot image: {path:?}"))?;
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
fn write_image(path: &Path, image: &BootImage) -> Result<()> {
|
||||
let file =
|
||||
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
image
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write boot image: {path:?}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush boot image: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_header(path: &Path) -> Result<BootImage> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read header TOML: {path:?}"))?;
|
||||
let image = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse header TOML: {path:?}"))?;
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
fn write_header(path: &Path, image: &BootImage) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(image)
|
||||
.with_context(|| format!("Failed to serialize header TOML: {path:?}"))?;
|
||||
fs::write(path, data).with_context(|| format!("Failed to write header TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_data_if_exists(path: &Path) -> Result<Option<Vec<u8>>> {
|
||||
let data = match fs::read(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to read data: {path:?}"))?,
|
||||
};
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
fn read_text_if_exists(path: &Path) -> Result<Option<String>> {
|
||||
let data = match fs::read_to_string(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to read text: {path:?}"))?,
|
||||
};
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
fn read_avb_header_if_exists(path: &Path) -> Result<Option<Header>> {
|
||||
let file = match File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?,
|
||||
};
|
||||
let header = Header::from_reader(BufReader::new(file))
|
||||
.with_context(|| format!("Failed to read vbmeta header: {path:?}"))?;
|
||||
|
||||
Ok(Some(header))
|
||||
}
|
||||
|
||||
fn write_data_if_not_empty(path: &Path, data: &[u8]) -> Result<()> {
|
||||
if !data.is_empty() {
|
||||
fs::write(path, data).with_context(|| format!("Failed to write data: {path:?}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_text_if_not_empty(path: &Path, text: &str) -> Result<()> {
|
||||
if !text.is_empty() {
|
||||
fs::write(path, text.as_bytes())
|
||||
.with_context(|| format!("Failed to write text: {path:?}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_avb_header(path: &Path, header: &Header) -> Result<()> {
|
||||
let file =
|
||||
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
header.to_writer(BufWriter::new(file))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_info(cli: &BootCli, image: &BootImage) {
|
||||
if !cli.quiet {
|
||||
if cli.debug {
|
||||
println!("{image:#?}");
|
||||
} else {
|
||||
println!("{image}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_subcommand(boot_cli: &BootCli, cli: &UnpackCli) -> Result<()> {
|
||||
let image = read_image(&cli.input)?;
|
||||
display_info(boot_cli, &image);
|
||||
|
||||
write_header(&cli.output_header, &image)?;
|
||||
|
||||
let mut kernel = None;
|
||||
let mut second = None;
|
||||
let mut recovery_dtbo = None;
|
||||
let mut dtb = None;
|
||||
let mut vts_signature = None;
|
||||
let mut bootconfig = None;
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
match &image {
|
||||
BootImage::V0Through2(b) => {
|
||||
kernel = Some(&b.kernel);
|
||||
second = Some(&b.second);
|
||||
if let Some(v1) = &b.v1_extra {
|
||||
recovery_dtbo = Some(&v1.recovery_dtbo);
|
||||
}
|
||||
if let Some(v2) = &b.v2_extra {
|
||||
dtb = Some(&v2.dtb);
|
||||
}
|
||||
ramdisks.push(&b.ramdisk);
|
||||
}
|
||||
BootImage::V3Through4(b) => {
|
||||
kernel = Some(&b.kernel);
|
||||
if let Some(v4) = &b.v4_extra {
|
||||
vts_signature = v4.signature.as_ref();
|
||||
}
|
||||
ramdisks.push(&b.ramdisk);
|
||||
}
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
dtb = Some(&b.dtb);
|
||||
if let Some(v4) = &b.v4_extra {
|
||||
bootconfig = Some(&v4.bootconfig);
|
||||
}
|
||||
ramdisks.extend(b.ramdisks.iter());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(data) = kernel {
|
||||
write_data_if_not_empty(&cli.output_kernel, data)?;
|
||||
}
|
||||
if let Some(data) = second {
|
||||
write_data_if_not_empty(&cli.output_second, data)?;
|
||||
}
|
||||
if let Some(data) = recovery_dtbo {
|
||||
write_data_if_not_empty(&cli.output_recovery_dtbo, data)?;
|
||||
}
|
||||
if let Some(data) = dtb {
|
||||
write_data_if_not_empty(&cli.output_dtb, data)?;
|
||||
}
|
||||
if let Some(header) = vts_signature {
|
||||
write_avb_header(&cli.output_vts_signature, header)?;
|
||||
}
|
||||
if let Some(text) = bootconfig {
|
||||
write_text_if_not_empty(&cli.output_bootconfig, text)?;
|
||||
}
|
||||
|
||||
for (i, data) in ramdisks.iter().enumerate() {
|
||||
let mut path = cli.output_ramdisk_prefix.as_os_str().to_owned();
|
||||
path.push(i.to_string());
|
||||
|
||||
write_data_if_not_empty(Path::new(&path), data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(boot_cli: &BootCli, cli: &PackCli) -> Result<()> {
|
||||
let mut image = read_header(&cli.input_header)?;
|
||||
|
||||
let kernel = read_data_if_exists(&cli.input_kernel)?;
|
||||
let second = read_data_if_exists(&cli.input_second)?;
|
||||
let recovery_dtbo = read_data_if_exists(&cli.input_recovery_dtbo)?;
|
||||
let dtb = read_data_if_exists(&cli.input_dtb)?;
|
||||
let vts_signature = read_avb_header_if_exists(&cli.input_vts_signature)?;
|
||||
let bootconfig = read_text_if_exists(&cli.input_bootconfig)?;
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
for i in 0.. {
|
||||
let mut path = cli.input_ramdisk_prefix.as_os_str().to_owned();
|
||||
path.push(i.to_string());
|
||||
|
||||
let Some(ramdisk) = read_data_if_exists(Path::new(&path))? else {
|
||||
break;
|
||||
};
|
||||
|
||||
ramdisks.push(ramdisk);
|
||||
}
|
||||
|
||||
match &mut image {
|
||||
BootImage::V0Through2(b) => {
|
||||
b.kernel = kernel.unwrap_or_default();
|
||||
b.second = second.unwrap_or_default();
|
||||
if let Some(v1) = &mut b.v1_extra {
|
||||
v1.recovery_dtbo = recovery_dtbo.unwrap_or_default();
|
||||
}
|
||||
if let Some(v2) = &mut b.v2_extra {
|
||||
v2.dtb = dtb.unwrap_or_default();
|
||||
}
|
||||
if ramdisks.len() > 1 {
|
||||
bail!("Image type only supports a single ramdisk");
|
||||
}
|
||||
b.ramdisk = ramdisks.into_iter().next().unwrap_or_default();
|
||||
}
|
||||
BootImage::V3Through4(b) => {
|
||||
b.kernel = kernel.unwrap_or_default();
|
||||
if let Some(v4) = &mut b.v4_extra {
|
||||
v4.signature = vts_signature;
|
||||
}
|
||||
if ramdisks.len() > 1 {
|
||||
bail!("Image type only supports a single ramdisk");
|
||||
}
|
||||
b.ramdisk = ramdisks.into_iter().next().unwrap_or_default();
|
||||
}
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
b.dtb = dtb.unwrap_or_default();
|
||||
if let Some(v4) = &mut b.v4_extra {
|
||||
v4.bootconfig = bootconfig.unwrap_or_default();
|
||||
}
|
||||
b.ramdisks = ramdisks;
|
||||
}
|
||||
}
|
||||
|
||||
display_info(boot_cli, &image);
|
||||
write_image(&cli.output, &image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(boot_cli: &BootCli, cli: &RepackCli) -> Result<()> {
|
||||
let image = read_image(&cli.input)?;
|
||||
display_info(boot_cli, &image);
|
||||
write_image(&cli.output, &image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(boot_cli: &BootCli, cli: &InfoCli) -> Result<()> {
|
||||
let image = read_image(&cli.input)?;
|
||||
display_info(boot_cli, &image);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
|
||||
let raw_reader = File::open(&cli.image)
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.image))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))
|
||||
.with_context(|| format!("Failed to load boot image: {:?}", cli.image))?;
|
||||
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
match &boot_image {
|
||||
BootImage::V0Through2(b) => {
|
||||
if !b.ramdisk.is_empty() {
|
||||
ramdisks.push(&b.ramdisk);
|
||||
}
|
||||
}
|
||||
BootImage::V3Through4(b) => {
|
||||
if !b.ramdisk.is_empty() {
|
||||
ramdisks.push(&b.ramdisk);
|
||||
}
|
||||
}
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
ramdisks.extend(b.ramdisks.iter());
|
||||
}
|
||||
}
|
||||
|
||||
for (i, ramdisk) in ramdisks.iter().enumerate() {
|
||||
let reader = Cursor::new(ramdisk);
|
||||
let reader = CompressedReader::new(reader, true)
|
||||
.with_context(|| format!("Failed to load ramdisk #{i}"))?;
|
||||
let mut cpio_reader = CpioReader::new(reader, false);
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Not a Magisk-patched boot image");
|
||||
}
|
||||
|
||||
pub fn boot_main(cli: &BootCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
BootCommand::Unpack(c) => unpack_subcommand(cli, c),
|
||||
BootCommand::Pack(c) => pack_subcommand(cli, c),
|
||||
BootCommand::Repack(c) => repack_subcommand(cli, c),
|
||||
BootCommand::Info(c) => info_subcommand(cli, c),
|
||||
BootCommand::MagiskInfo(c) => magisk_info_subcommand(c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack a boot image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input boot image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output header TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
|
||||
output_header: PathBuf,
|
||||
|
||||
/// Path to output kernel image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "kernel.img")]
|
||||
output_kernel: PathBuf,
|
||||
|
||||
/// Path prefix for output ramdisk images.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "ramdisk.img."
|
||||
)]
|
||||
output_ramdisk_prefix: PathBuf,
|
||||
|
||||
/// Path to output second stage bootloader image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "second.img")]
|
||||
output_second: PathBuf,
|
||||
|
||||
/// Path to output recovery dtbo/acpio image.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "recovery_dtbo.img"
|
||||
)]
|
||||
output_recovery_dtbo: PathBuf,
|
||||
|
||||
/// Path to output device tree blob image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "dtb.img")]
|
||||
output_dtb: PathBuf,
|
||||
|
||||
/// Path to output VTS signature.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "vts_signature.img"
|
||||
)]
|
||||
output_vts_signature: PathBuf,
|
||||
|
||||
/// Path to output bootconfig text.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "bootconfig.txt"
|
||||
)]
|
||||
output_bootconfig: PathBuf,
|
||||
}
|
||||
|
||||
/// Pack a boot image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output boot image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input header TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
|
||||
input_header: PathBuf,
|
||||
|
||||
/// Path to input kernel image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "kernel.img")]
|
||||
input_kernel: PathBuf,
|
||||
|
||||
/// Path prefix for input ramdisk images.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "ramdisk.img."
|
||||
)]
|
||||
input_ramdisk_prefix: PathBuf,
|
||||
|
||||
/// Path to input second stage bootloader image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "second.img")]
|
||||
input_second: PathBuf,
|
||||
|
||||
/// Path to input recovery dtbo/acpio image.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "recovery_dtbo.img"
|
||||
)]
|
||||
input_recovery_dtbo: PathBuf,
|
||||
|
||||
/// Path to input device tree blob image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "dtb.img")]
|
||||
input_dtb: PathBuf,
|
||||
|
||||
/// Path to input VTS signature.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "vts_signature.img"
|
||||
)]
|
||||
input_vts_signature: PathBuf,
|
||||
|
||||
/// Path to input bootconfig text.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "bootconfig.txt"
|
||||
)]
|
||||
input_bootconfig: PathBuf,
|
||||
}
|
||||
|
||||
/// Repack a boot image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input boot image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output boot image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
/// Display boot image header information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input boot image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
/// Print Magisk config from a patched boot image.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MagiskInfoCli {
|
||||
/// Path to Magisk-patched boot image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
pub image: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum BootCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
MagiskInfo(MagiskInfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect boot images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct BootCli {
|
||||
#[command(subcommand)]
|
||||
command: BootCommand,
|
||||
|
||||
/// Don't print boot image header information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
|
||||
/// Print boot image header information in debug format.
|
||||
#[arg(short, long, global = true)]
|
||||
debug: bool,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_complete::Shell;
|
||||
|
||||
use crate::cli::args::Cli;
|
||||
|
||||
pub fn completion_main(cli: &CompletionCli) -> Result<()> {
|
||||
clap_complete::generate(
|
||||
cli.shell,
|
||||
&mut Cli::command(),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
&mut io::stdout(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate shell tab completion configs.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CompletionCli {
|
||||
/// The shell to generate completions for.
|
||||
#[arg(short, long, value_name = "SHELL", value_parser)]
|
||||
pub shell: Shell,
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* 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 cpio 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,191 @@
|
||||
/*
|
||||
* 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,176 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::hashtree::HashTreeImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
fn read_hash_tree(path: &Path) -> Result<HashTreeImage> {
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let hash_tree = HashTreeImage::from_reader(reader)
|
||||
.with_context(|| format!("Failed to read hash tree data: {path:?}"))?;
|
||||
|
||||
Ok(hash_tree)
|
||||
}
|
||||
|
||||
fn write_hash_tree(path: &Path, hash_tree: &HashTreeImage) -> Result<()> {
|
||||
let mut writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
hash_tree
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write hash tree data: {path:?}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush hash tree data: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let salt = hex::decode(&cli.salt).context("Invalid salt")?;
|
||||
let input = open_input(&cli.input, false)?;
|
||||
|
||||
let hash_tree =
|
||||
HashTreeImage::generate(&input, cli.block_size, &cli.algorithm, &salt, cancel_signal)
|
||||
.context("Failed to generate hash tree data")?;
|
||||
|
||||
write_hash_tree(&cli.hash_tree, &hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let ranges = cli
|
||||
.range
|
||||
.chunks_exact(2)
|
||||
.map(|w| w[0]..w[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let mut hash_tree = read_hash_tree(&cli.hash_tree)?;
|
||||
|
||||
hash_tree
|
||||
.update(&input, &ranges, cancel_signal)
|
||||
.context("Failed to update hash tree data")?;
|
||||
|
||||
write_hash_tree(&cli.hash_tree, &hash_tree)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let hash_tree = read_hash_tree(&cli.hash_tree)?;
|
||||
|
||||
hash_tree
|
||||
.verify(&input, cancel_signal)
|
||||
.context("Failed to verify data")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash_tree_main(cli: &HashTreeCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
HashTreeCommand::Generate(c) => generate_subcommand(c, cancel_signal),
|
||||
HashTreeCommand::Update(c) => update_subcommand(c, cancel_signal),
|
||||
HashTreeCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output hash tree data.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
|
||||
/// Block size.
|
||||
#[arg(short, long, value_name = "BYTES", default_value = "4096")]
|
||||
block_size: u32,
|
||||
|
||||
/// Hash algorithm.
|
||||
#[arg(short, long, value_name = "NAME", default_value = "sha256")]
|
||||
algorithm: String,
|
||||
|
||||
/// Salt (in hex).
|
||||
#[arg(short, long, value_name = "HEX", default_value = "")]
|
||||
salt: String,
|
||||
}
|
||||
|
||||
/// Update hash tree data after a file is modified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UpdateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to hash tree data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
|
||||
/// Input file ranges that were updated.
|
||||
///
|
||||
/// This is a half-open range and can be specified multiple times.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
range: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Verify that a file contains no errors.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input hash tree data.
|
||||
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
|
||||
hash_tree: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum HashTreeCommand {
|
||||
Generate(GenerateCli),
|
||||
Update(UpdateCli),
|
||||
Verify(VerifyCli),
|
||||
}
|
||||
|
||||
/// Generate dm-verity hash tree data and verify files.
|
||||
///
|
||||
/// These commands operate on a standard hash tree data prepended by a custom
|
||||
/// header.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct HashTreeCli {
|
||||
#[command(subcommand)]
|
||||
command: HashTreeCommand,
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
format::avb,
|
||||
};
|
||||
|
||||
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 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, &source)
|
||||
.with_context(|| format!("Failed to write private key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::GenerateCert(c) => {
|
||||
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(|| format!("Failed to write certificate: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::ExtractAvb(c) => {
|
||||
let public_key = if let Some(p) = &c.input.key {
|
||||
let passphrase = get_passphrase_source(&c.passphrase, p);
|
||||
let private_key = crypto::read_pem_key_file(p, &passphrase)
|
||||
.with_context(|| format!("Failed to load key: {p:?}"))?;
|
||||
|
||||
private_key.to_public_key()
|
||||
} else if let Some(p) = &c.input.cert {
|
||||
let certificate = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
|
||||
crypto::get_public_key(&certificate)
|
||||
.with_context(|| format!("Failed to extract public key: {p:?}"))?
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let encoded = avb::encode_public_key(&public_key)
|
||||
.context("Failed to encode public key in AVB format")?;
|
||||
|
||||
fs::write(&c.output, encoded)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::DecodeAvb(c) => {
|
||||
let encoded = fs::read(&c.key)
|
||||
.with_context(|| format!("Failed to load AVB public key: {:?}", c.key))?;
|
||||
|
||||
let public_key = avb::decode_public_key(&encoded)
|
||||
.context("Failed to decode public key as AVB format")?;
|
||||
|
||||
crypto::write_pem_public_key_file(&c.output, &public_key)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
struct PublicKeyInputGroup {
|
||||
/// Path to private key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct PassphraseGroup {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Generate an 4096-bit RSA keypair.
|
||||
///
|
||||
/// The output is saved in the standard PKCS8 format.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateKeyCli {
|
||||
/// Path to output private key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
passphrase: PassphraseGroup,
|
||||
}
|
||||
|
||||
/// Generate a certificate.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateCertCli {
|
||||
/// Path to input private key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
passphrase: PassphraseGroup,
|
||||
|
||||
/// Path to output certificate.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Certificate subject with comma-separated components.
|
||||
#[arg(short, long, default_value = "CN=avbroot")]
|
||||
subject: String,
|
||||
|
||||
/// Certificate validity in days.
|
||||
#[arg(short, long, default_value = "10000")]
|
||||
validity: u64,
|
||||
}
|
||||
|
||||
/// Extract the AVB public key from a private key or certificate.
|
||||
///
|
||||
/// The public key is stored in both the private key and the certificate. Either
|
||||
/// one can be used interchangeably.
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExtractAvbCli {
|
||||
/// Path to output AVB public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
input: PublicKeyInputGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
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.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct KeyCli {
|
||||
#[command(subcommand)]
|
||||
command: KeyCommand,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 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 ota;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
env::{self, VarError},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cms::{
|
||||
cert::{CertificateChoices, IssuerAndSerialNumber},
|
||||
content_info::{CmsVersion, ContentInfo},
|
||||
signed_data::{
|
||||
CertificateSet, DigestAlgorithmIdentifiers, EncapsulatedContentInfo, SignatureValue,
|
||||
SignedData, SignerIdentifier, SignerInfo, SignerInfos,
|
||||
},
|
||||
};
|
||||
use pkcs8::{
|
||||
pkcs5::{pbes2, scrypt},
|
||||
DecodePrivateKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo, LineEnding,
|
||||
PrivateKeyInfo,
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rsa::{pkcs1v15::SigningKey, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
use x509_cert::{
|
||||
builder::{Builder, CertificateBuilder, Profile},
|
||||
der::{pem::PemLabel, referenced::OwnedToRef, Any, Decode, DecodePem, EncodePem},
|
||||
serial_number::SerialNumber,
|
||||
spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned},
|
||||
time::Validity,
|
||||
Certificate,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Passphrases do not match")]
|
||||
ConfirmPassphrase,
|
||||
#[error("Failed to read environment variable: {0:?}")]
|
||||
InvalidEnvVar(OsString, #[source] VarError),
|
||||
#[error("PEM has start tag, but no end tag")]
|
||||
PemNoEndTag,
|
||||
#[error("Failed to load encrypted private key")]
|
||||
LoadKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to load unencrypted private key")]
|
||||
LoadKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save encrypted private key")]
|
||||
SaveKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save unencrypted private key")]
|
||||
SaveKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("X509 error")]
|
||||
X509(#[from] x509_cert::builder::Error),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub enum PassphraseSource {
|
||||
Prompt(String),
|
||||
EnvVar(OsString),
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
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:?}: "))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquire(&self, confirm: bool) -> Result<String> {
|
||||
let passphrase = match self {
|
||||
Self::Prompt(p) => {
|
||||
let first = rpassword::prompt_password(p)?;
|
||||
|
||||
if confirm {
|
||||
let second = rpassword::prompt_password("Confirm: ")?;
|
||||
|
||||
if first != second {
|
||||
return Err(Error::ConfirmPassphrase);
|
||||
}
|
||||
}
|
||||
|
||||
first
|
||||
}
|
||||
Self::EnvVar(v) => env::var(v).map_err(|e| Error::InvalidEnvVar(v.clone(), e))?,
|
||||
Self::File(p) => fs::read_to_string(p)?
|
||||
.trim_end_matches(&['\r', '\n'])
|
||||
.to_owned(),
|
||||
};
|
||||
|
||||
Ok(passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an 4096-bit RSA key pair.
|
||||
pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// avbroot supports 4096-bit keys only.
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Generate a self-signed certificate.
|
||||
pub fn generate_cert(
|
||||
key: &RsaPrivateKey,
|
||||
serial: u64,
|
||||
validity: Duration,
|
||||
subject: &str,
|
||||
) -> Result<Certificate> {
|
||||
let public_key_der = key.to_public_key().to_public_key_der()?;
|
||||
let signing_key = SigningKey::<Sha256>::new(key.clone());
|
||||
|
||||
let builder = CertificateBuilder::new(
|
||||
Profile::Root,
|
||||
SerialNumber::from(serial),
|
||||
Validity::from_now(validity)?,
|
||||
subject.parse()?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())?,
|
||||
&signing_key,
|
||||
)?;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let cert = builder.build_with_rng(&mut rng)?;
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
/// x509_cert/pem follow rfc7468 strictly instead of implementing a lenient
|
||||
/// parser. The PEM decoder rejects lines in the base64 section that are longer
|
||||
/// than 64 characters, excluding whitespace. We'll reformat the data to deal
|
||||
/// with this because there are certificates that do not follow the spec, like
|
||||
/// the signing cert for the Pixel 7 Pro official OTAs.
|
||||
fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut result = vec![];
|
||||
let mut base64 = vec![];
|
||||
let mut inside_base64 = false;
|
||||
|
||||
for mut line in data.split(|&c| c == b'\n') {
|
||||
while !line.is_empty() && line[line.len() - 1].is_ascii_whitespace() {
|
||||
line = &line[..line.len() - 1];
|
||||
}
|
||||
|
||||
if line.is_empty() {
|
||||
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;
|
||||
|
||||
for chunk in base64.chunks(64) {
|
||||
result.extend_from_slice(chunk);
|
||||
result.push(b'\n');
|
||||
}
|
||||
|
||||
base64.clear();
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
} else if inside_base64 {
|
||||
base64.extend_from_slice(line);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if inside_base64 {
|
||||
return Err(Error::PemNoEndTag);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a reader.
|
||||
pub fn read_pem_cert(mut reader: impl Read) -> Result<Certificate> {
|
||||
let mut data = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
|
||||
let data = reformat_pem(&data)?;
|
||||
let certificate = Certificate::from_pem(data)?;
|
||||
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a writer.
|
||||
pub fn write_pem_cert(mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a file.
|
||||
pub fn read_pem_cert_file(path: &Path) -> Result<Certificate> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
read_pem_cert(reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a file.
|
||||
pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
|
||||
write_pem_cert(writer, cert)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a writer.
|
||||
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key.to_public_key_pem(LineEnding::LF)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
|
||||
if data.contains("ENCRYPTED") {
|
||||
let passphrase = source.acquire(false)?;
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(&data, passphrase).map_err(Error::LoadKeyEncrypted)
|
||||
} else {
|
||||
RsaPrivateKey::from_pkcs8_pem(&data).map_err(Error::LoadKeyUnencrypted)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 private key to a writer.
|
||||
pub fn write_pem_key(
|
||||
mut writer: impl Write,
|
||||
key: &RsaPrivateKey,
|
||||
source: &PassphraseSource,
|
||||
) -> Result<()> {
|
||||
let passphrase = source.acquire(true)?;
|
||||
|
||||
let data = if passphrase.is_empty() {
|
||||
key.to_pkcs8_pem(LineEnding::LF)
|
||||
.map_err(Error::SaveKeyUnencrypted)?
|
||||
} else {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Normally, we'd just use key.to_pkcs8_encrypted_pem(). However, it
|
||||
// uses scrypt with n = 32768. This is high enough that openssl can no
|
||||
// longer read the file and craps out with `memory limit exceeded`.
|
||||
// Although we can read those files just fine, let's match openssl's
|
||||
// default parameters for better compatibility.
|
||||
//
|
||||
// Per `man openssl-pkcs8`: -scrypt Uses the scrypt algorithm for
|
||||
// private key encryption using default parameters: currently N=16384,
|
||||
// r=8 and p=1 and AES in CBC mode with a 256 bit key.
|
||||
//
|
||||
// https://github.com/RustCrypto/formats/issues/1205
|
||||
|
||||
let mut salt = [0u8; 16];
|
||||
rng.fill_bytes(&mut salt);
|
||||
|
||||
let mut iv = [0u8; 16];
|
||||
rng.fill_bytes(&mut iv);
|
||||
|
||||
// 14 = log_2(16384), 32 bytes = 256 bits
|
||||
let scrypt_params = scrypt::Params::new(14, 8, 1, 32).unwrap();
|
||||
let pbes2_params = pbes2::Parameters::scrypt_aes256cbc(scrypt_params, &salt, &iv).unwrap();
|
||||
|
||||
let plain_text_der = key.to_pkcs8_der().map_err(Error::SaveKeyEncrypted)?;
|
||||
let private_key_info =
|
||||
PrivateKeyInfo::try_from(plain_text_der.as_bytes()).map_err(Error::SaveKeyEncrypted)?;
|
||||
|
||||
let secret_doc = private_key_info
|
||||
.encrypt_with_params(pbes2_params, passphrase)
|
||||
.map_err(Error::SaveKeyEncrypted)?;
|
||||
|
||||
secret_doc.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)?
|
||||
};
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a file.
|
||||
pub fn read_pem_key_file(path: &Path, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
read_pem_key(reader, source)
|
||||
}
|
||||
|
||||
/// Save PEM-encoded PKCS8 private key to a file.
|
||||
pub fn write_pem_key_file(
|
||||
path: &Path,
|
||||
key: &RsaPrivateKey,
|
||||
source: &PassphraseSource,
|
||||
) -> Result<()> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true);
|
||||
options.create(true);
|
||||
options.truncate(true);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let file = options.open(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
|
||||
write_pem_key(writer, key, source)
|
||||
}
|
||||
|
||||
/// Get the RSA public key from a certificate.
|
||||
pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
|
||||
let public_key =
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())?;
|
||||
|
||||
Ok(public_key)
|
||||
}
|
||||
|
||||
/// Check if a certificate matches a private key.
|
||||
pub fn cert_matches_key(cert: &Certificate, key: &RsaPrivateKey) -> Result<bool> {
|
||||
let public_key = get_public_key(cert)?;
|
||||
|
||||
Ok(key.to_public_key() == public_key)
|
||||
}
|
||||
|
||||
/// Parse a CMS [`SignedData`] structure from raw DER-encoded data.
|
||||
pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
|
||||
let ci = ContentInfo::from_der(data)?;
|
||||
let sd = ci.content.decode_as::<SignedData>()?;
|
||||
|
||||
Ok(sd)
|
||||
}
|
||||
|
||||
/// Get a list of all standard X509 certificates contained within a
|
||||
/// [`SignedData`] structure.
|
||||
pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
|
||||
sd.certificates.as_ref().map_or_else(Vec::new, |certs| {
|
||||
certs
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a CMS signature from an external digest. This implementation does not
|
||||
/// use signed attributes because AOSP recovery's otautil/verifier.cpp is not
|
||||
/// actually CMS compliant. It simply uses the CMS [`SignedData`] structure as
|
||||
/// 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,
|
||||
cert: &Certificate,
|
||||
digest: &[u8],
|
||||
) -> Result<ContentInfo> {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
let signature = key.sign(scheme, digest)?;
|
||||
|
||||
let digest_algorithm = AlgorithmIdentifierOwned {
|
||||
oid: const_oid::db::rfc5912::ID_SHA_256,
|
||||
parameters: None,
|
||||
};
|
||||
|
||||
let signed_data = SignedData {
|
||||
version: CmsVersion::V1,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])?,
|
||||
encap_content_info: EncapsulatedContentInfo {
|
||||
econtent_type: const_oid::db::rfc5911::ID_DATA,
|
||||
econtent: None,
|
||||
},
|
||||
certificates: Some(CertificateSet::try_from(vec![
|
||||
CertificateChoices::Certificate(cert.clone()),
|
||||
])?),
|
||||
crls: None,
|
||||
signer_infos: SignerInfos::try_from(vec![SignerInfo {
|
||||
version: CmsVersion::V1,
|
||||
sid: SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber {
|
||||
issuer: cert.tbs_certificate.issuer.clone(),
|
||||
serial_number: cert.tbs_certificate.serial_number.clone(),
|
||||
}),
|
||||
digest_alg: digest_algorithm,
|
||||
signed_attrs: None,
|
||||
signature_algorithm: AlgorithmIdentifierOwned {
|
||||
oid: const_oid::db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
|
||||
parameters: None,
|
||||
},
|
||||
signature: SignatureValue::new(signature)?,
|
||||
unsigned_attrs: None,
|
||||
}])?,
|
||||
};
|
||||
|
||||
let signed_data = ContentInfo {
|
||||
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
|
||||
content: Any::encode_from(&signed_data)?,
|
||||
};
|
||||
|
||||
Ok(signed_data)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct Lz4LegacyEncoder<W: Write> {
|
||||
writer: Option<W>,
|
||||
buf: Vec<u8>,
|
||||
n_filled: usize,
|
||||
}
|
||||
|
||||
impl<W: Write> Lz4LegacyEncoder<W> {
|
||||
pub fn new(mut writer: W) -> io::Result<Self> {
|
||||
writer.write_all(LZ4_LEGACY_MAGIC)?;
|
||||
|
||||
Ok(Self {
|
||||
writer: Some(writer),
|
||||
// We always use the max block size.
|
||||
buf: vec![0u8; 8 * 1024 * 1024],
|
||||
n_filled: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_block(&mut self, force: bool) -> io::Result<()> {
|
||||
if !force && self.n_filled < self.buf.len() {
|
||||
// Block not fully filled yet.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// HC is currently not supported:
|
||||
// https://github.com/PSeitz/lz4_flex/issues/21
|
||||
let compressed = lz4_flex::block::compress(&self.buf[..self.n_filled]);
|
||||
|
||||
let writer = self.writer.as_mut().unwrap();
|
||||
writer.write_u32::<LittleEndian>(compressed.len() as u32)?;
|
||||
writer.write_all(&compressed)?;
|
||||
|
||||
self.n_filled = 0;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> io::Result<W> {
|
||||
self.write_block(true)?;
|
||||
Ok(self.writer.take().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Drop for Lz4LegacyEncoder<W> {
|
||||
fn drop(&mut self) {
|
||||
if self.writer.is_some() {
|
||||
let _ = self.write_block(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for Lz4LegacyEncoder<W> {
|
||||
fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
|
||||
let total = buf.len();
|
||||
|
||||
while !buf.is_empty() {
|
||||
let to_write = buf.len().min(self.buf.len() - self.n_filled);
|
||||
self.buf[self.n_filled..self.n_filled + to_write].copy_from_slice(&buf[..to_write]);
|
||||
|
||||
self.n_filled += to_write;
|
||||
self.write_block(false)?;
|
||||
|
||||
buf = &buf[to_write..];
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.write_block(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[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; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
reader.rewind()?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
} 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 {
|
||||
Err(Error::UnknownFormat)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> CompressedFormat {
|
||||
match self {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
match self {
|
||||
Self::None(r) => r,
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for CompressedReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::None(r) => r.read(buf),
|
||||
Self::Gzip(r) => r.read(buf),
|
||||
Self::Lz4(r) => r.read(buf),
|
||||
Self::Xz(r) => r.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CompressedWriter<W: Write> {
|
||||
None(W),
|
||||
Gzip(GzEncoder<W>),
|
||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||
Xz(XzEncoder<W>),
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedWriter<W> {
|
||||
pub fn new(writer: W, format: CompressedFormat) -> Result<Self> {
|
||||
match format {
|
||||
CompressedFormat::None => Ok(Self::None(writer)),
|
||||
CompressedFormat::Gzip => {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> CompressedFormat {
|
||||
match self {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> io::Result<W> {
|
||||
match self {
|
||||
Self::None(w) => Ok(w),
|
||||
Self::Gzip(w) => w.finish(),
|
||||
Self::Lz4Legacy(w) => w.finish(),
|
||||
Self::Xz(w) => w.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for CompressedWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::None(w) => w.write(buf),
|
||||
Self::Gzip(w) => w.write(buf),
|
||||
Self::Lz4Legacy(w) => w.write(buf),
|
||||
Self::Xz(w) => w.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::None(w) => w.flush(),
|
||||
Self::Gzip(w) => w.flush(),
|
||||
Self::Lz4Legacy(w) => w.flush(),
|
||||
Self::Xz(w) => w.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
escape,
|
||||
format::padding,
|
||||
octal,
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt,
|
||||
},
|
||||
util::NumBytes,
|
||||
};
|
||||
|
||||
const MAGIC_NEW: &[u8; 6] = b"070701";
|
||||
const MAGIC_NEW_CRC: &[u8; 6] = b"070702";
|
||||
|
||||
const CPIO_TRAILER: &[u8; 10] = b"TRAILER!!!";
|
||||
|
||||
const S_IFIFO: u32 = 0o010000;
|
||||
const S_IFCHR: u32 = 0o020000;
|
||||
const S_IFDIR: u32 = 0o040000;
|
||||
const S_IFBLK: u32 = 0o060000;
|
||||
const S_IFREG: u32 = 0o100000;
|
||||
const S_IFLNK: u32 = 0o120000;
|
||||
const S_IFSOCK: u32 = 0o140000;
|
||||
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.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")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Read u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn read_int(mut reader: impl Read) -> io::Result<u32> {
|
||||
let mut buf = [0u8; 8];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut value = 0;
|
||||
|
||||
for b in buf {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
|
||||
)
|
||||
})?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Write u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
writer.write_all(&buf)
|
||||
}
|
||||
|
||||
/// 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, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CpioEntryType {
|
||||
Pipe,
|
||||
Char,
|
||||
Directory,
|
||||
Block,
|
||||
Regular,
|
||||
Symlink,
|
||||
Socket,
|
||||
Reserved,
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
.field("mtime", &self.mtime)
|
||||
.field("dev_maj", &self.dev_maj)
|
||||
.field("dev_min", &self.dev_min)
|
||||
.field("rdev_maj", &self.rdev_maj)
|
||||
.field("rdev_min", &self.rdev_min)
|
||||
.field("crc32", &self.crc32)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CpioEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
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 CpioEntry {
|
||||
pub fn new_trailer() -> Self {
|
||||
Self {
|
||||
path: CPIO_TRAILER.to_vec(),
|
||||
// Must be 1 for CRC format.
|
||||
nlink: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_symlink(path: &[u8], link_target: &[u8]) -> Self {
|
||||
Self {
|
||||
path: path.to_owned(),
|
||||
data: CpioEntryData::Data(link_target.to_owned()),
|
||||
file_type: CpioEntryType::Symlink,
|
||||
file_mode: 0o777,
|
||||
nlink: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_directory(path: &[u8], mode: u16) -> Self {
|
||||
Self {
|
||||
path: path.to_owned(),
|
||||
file_type: CpioEntryType::Directory,
|
||||
file_mode: mode,
|
||||
nlink: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_file(path: &[u8], mode: u16, data: CpioEntryData) -> Self {
|
||||
Self {
|
||||
path: path.to_owned(),
|
||||
data,
|
||||
file_type: CpioEntryType::Regular,
|
||||
file_mode: mode,
|
||||
nlink: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_trailer(&self) -> bool {
|
||||
self.path == CPIO_TRAILER
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for CpioEntry {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
let mut reader = CountingReader::new(reader);
|
||||
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
if magic != *MAGIC_NEW && magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
}
|
||||
|
||||
let inode = read_int(&mut reader)?;
|
||||
let mode = read_int(&mut reader)?;
|
||||
let uid = read_int(&mut reader)?;
|
||||
let gid = read_int(&mut reader)?;
|
||||
let nlink = read_int(&mut reader)?;
|
||||
let mtime = read_int(&mut reader)?;
|
||||
let file_size = read_int(&mut reader)?;
|
||||
let dev_maj = read_int(&mut reader)?;
|
||||
let dev_min = read_int(&mut reader)?;
|
||||
let rdev_maj = read_int(&mut reader)?;
|
||||
let rdev_min = read_int(&mut reader)?;
|
||||
let path_size = read_int(&mut reader)?;
|
||||
let crc32 = read_int(&mut reader)?;
|
||||
|
||||
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());
|
||||
}
|
||||
path.pop();
|
||||
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 {
|
||||
path,
|
||||
data,
|
||||
inode,
|
||||
file_type,
|
||||
file_mode: (mode & 0o7777) as u16,
|
||||
uid,
|
||||
gid,
|
||||
nlink,
|
||||
mtime,
|
||||
dev_maj,
|
||||
dev_min,
|
||||
rdev_maj,
|
||||
rdev_min,
|
||||
crc32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, writer: W) -> Result<()> {
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let path_size = self
|
||||
.path
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.and_then(|s| s.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
|
||||
|
||||
write_int(&mut writer, self.inode)?;
|
||||
write_int(&mut writer, mode)?;
|
||||
write_int(&mut writer, self.uid)?;
|
||||
write_int(&mut writer, self.gid)?;
|
||||
write_int(&mut writer, self.nlink)?;
|
||||
write_int(&mut writer, self.mtime)?;
|
||||
write_int(&mut writer, file_size)?;
|
||||
write_int(&mut writer, self.dev_maj)?;
|
||||
write_int(&mut writer, self.dev_min)?;
|
||||
write_int(&mut writer, self.rdev_maj)?;
|
||||
write_int(&mut writer, self.rdev_min)?;
|
||||
write_int(&mut writer, path_size)?;
|
||||
write_int(&mut writer, self.crc32)?;
|
||||
|
||||
writer.write_all(&self.path)?;
|
||||
writer.write_zeros_exact(1)?;
|
||||
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 struct CpioReader<R: Read> {
|
||||
reader: R,
|
||||
include_trailer: bool,
|
||||
range: Option<Range<u64>>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn sort(entries: &mut [CpioEntry]) {
|
||||
entries.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// (dev maj, dev min) -> (inode set, last assigned inode)
|
||||
let mut inodes: HashMap<(u32, u32), (HashSet<u32>, u32)> = HashMap::new();
|
||||
|
||||
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,933 @@
|
||||
/*
|
||||
* 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,744 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
format::{avb, padding},
|
||||
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
|
||||
util::{self, NumBytes},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Hash tree should have size {expected} for input size {input}, but has size {actual}")]
|
||||
InvalidHashTreeSize {
|
||||
input: u64,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("Expected root digest {expected}, but have {actual}")]
|
||||
InvalidRootDigest { expected: String, actual: String },
|
||||
#[error("Expected hash tree {expected}, but have {actual}")]
|
||||
InvalidHashTree { expected: String, actual: String },
|
||||
#[error("Invalid hash tree header magic: {:?}", .0.as_bstr())]
|
||||
InvalidHeaderMagic([u8; 16]),
|
||||
#[error("Invalid hash tree header version: {0}")]
|
||||
InvalidHeaderVersion(u16),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct HashTree {
|
||||
block_size: u32,
|
||||
salted_context: Context,
|
||||
}
|
||||
|
||||
impl HashTree {
|
||||
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &[u8]) -> Self {
|
||||
let mut salted_context = Context::new(algorithm);
|
||||
salted_context.update(salt);
|
||||
|
||||
Self {
|
||||
block_size,
|
||||
salted_context,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of offset ranges that each level occupies in the hash
|
||||
/// tree data. The items are returned with the bottom level's offsets first
|
||||
/// in the list. Note that the bottom level is stored at the end of the hash
|
||||
/// tree data.
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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 ota;
|
||||
pub mod padding;
|
||||
pub mod payload;
|
||||
pub mod verityrs;
|
||||
@@ -0,0 +1,752 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
iter,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
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,
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter},
|
||||
};
|
||||
|
||||
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
|
||||
pub const PATH_METADATA_PB: &str = "META-INF/com/android/metadata.pb";
|
||||
pub const PATH_OTACERT: &str = "META-INF/com/android/otacert";
|
||||
pub const PATH_PAYLOAD: &str = "payload.bin";
|
||||
pub const PATH_PROPERTIES: &str = "payload_properties.txt";
|
||||
|
||||
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";
|
||||
|
||||
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")]
|
||||
OtaMagicNotFound,
|
||||
#[error("Cannot find EOCD magic")]
|
||||
EocdMagicNotFound,
|
||||
#[error("EOCD magic found in archive comment")]
|
||||
EocdMagicInComment,
|
||||
#[error("Zip is too small to contain EOCD")]
|
||||
ZipTooSmall,
|
||||
#[error("Signature offset exceeds archive comment size")]
|
||||
SignatureOffsetTooLarge,
|
||||
#[error("Expected exactly one CMS embedded certificate, but found {0}")]
|
||||
NotOneCmsCertificate(usize),
|
||||
#[error("Expected exactly one CMS SignerInfo, but found {0}")]
|
||||
NotOneCmsSignerInfo(usize),
|
||||
#[error("Unsupported digest algorithm: {0}")]
|
||||
UnsupportedDigestAlgorithm(ObjectIdentifier),
|
||||
#[error("Unsupported signature algorithm: {0}")]
|
||||
UnsupportedSignatureAlgorithm(ObjectIdentifier),
|
||||
#[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:?}")]
|
||||
InvalidPropertyFileEntry(String),
|
||||
#[error("Missing entry in OTA zip: {0}")]
|
||||
MissingZipEntry(&'static str),
|
||||
#[error("CMS signing error")]
|
||||
CmsSign(#[from] crypto::Error),
|
||||
#[error("Payload error")]
|
||||
Payload(#[from] payload::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")]
|
||||
Io(#[from] io::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>)> {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut pairs = BTreeMap::<String, String>::new();
|
||||
|
||||
// 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());
|
||||
}
|
||||
if metadata.retrofit_dynamic_partitions {
|
||||
pairs.insert(
|
||||
"ota-retrofit-dynamic-partitions".to_owned(),
|
||||
"yes".to_owned(),
|
||||
);
|
||||
}
|
||||
if metadata.downgrade {
|
||||
pairs.insert("ota-downgrade".to_owned(), "yes".to_owned());
|
||||
}
|
||||
|
||||
pairs.insert(
|
||||
"ota-required-cache".to_owned(),
|
||||
metadata.required_cache.to_string(),
|
||||
);
|
||||
|
||||
if let Some(p) = &metadata.postcondition {
|
||||
pairs.insert("post-build".to_owned(), p.build.join(LEGACY_SEP));
|
||||
pairs.insert(
|
||||
"post-build-incremental".to_owned(),
|
||||
p.build_incremental.clone(),
|
||||
);
|
||||
pairs.insert("post-sdk-level".to_owned(), p.sdk_level.clone());
|
||||
pairs.insert(
|
||||
"post-security-patch-level".to_owned(),
|
||||
p.security_patch_level.clone(),
|
||||
);
|
||||
pairs.insert("post-timestamp".to_owned(), p.timestamp.to_string());
|
||||
}
|
||||
|
||||
if let Some(p) = &metadata.precondition {
|
||||
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(LEGACY_SEP));
|
||||
pairs.insert(
|
||||
"pre-build-incremental".to_owned(),
|
||||
p.build_incremental.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if metadata.spl_downgrade {
|
||||
pairs.insert("spl-downgrade".to_owned(), "yes".to_owned());
|
||||
}
|
||||
|
||||
pairs.extend(metadata.property_files.clone());
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ZipEntry {
|
||||
pub name: String,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
/// Parse OTA property files string.
|
||||
pub fn parse_property_files(data: &str) -> Result<Vec<ZipEntry>> {
|
||||
let mut result = vec![];
|
||||
|
||||
for entry in data.trim_end().split(',') {
|
||||
let mut pieces = entry.split(':');
|
||||
|
||||
let name = pieces
|
||||
.next()
|
||||
.map(|p| p.to_owned())
|
||||
.ok_or_else(|| Error::InvalidPropertyFileEntry(entry.to_owned()))?;
|
||||
let offset = pieces
|
||||
.next()
|
||||
.and_then(|p| p.parse::<u64>().ok())
|
||||
.ok_or_else(|| Error::InvalidPropertyFileEntry(entry.to_owned()))?;
|
||||
let size = pieces
|
||||
.next()
|
||||
.and_then(|p| p.parse::<u64>().ok())
|
||||
.ok_or_else(|| Error::InvalidPropertyFileEntry(entry.to_owned()))?;
|
||||
|
||||
if pieces.next().is_some() {
|
||||
return Err(Error::InvalidPropertyFileEntry(entry.to_owned()));
|
||||
}
|
||||
|
||||
result.push(ZipEntry { name, offset, size });
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Compute the property files entries listing the offsets and sizes to every
|
||||
/// zip entry.
|
||||
fn compute_property_files(
|
||||
pf_name: &str,
|
||||
entries: &[ZipEntry],
|
||||
max_length: Option<usize>,
|
||||
) -> Result<String> {
|
||||
let compute = |path: &'static str| -> Result<String> {
|
||||
let entry = entries
|
||||
.iter()
|
||||
.find(|e| e.name == path)
|
||||
.ok_or_else(|| Error::MissingZipEntry(path))?;
|
||||
let name = path.rsplit_once('/').map_or(path, |p| p.1);
|
||||
|
||||
Ok(format!("{name}:{}:{}", entry.offset, entry.size))
|
||||
};
|
||||
|
||||
let mut tokens = vec![];
|
||||
|
||||
if pf_name == PF_NAME {
|
||||
tokens.push(compute(NAME_PAYLOAD_METADATA)?);
|
||||
}
|
||||
|
||||
for path in [PATH_PAYLOAD, PATH_PROPERTIES] {
|
||||
tokens.push(compute(path)?);
|
||||
}
|
||||
|
||||
for path in [
|
||||
"apex_info.pb",
|
||||
"care_map.pb",
|
||||
"care_map.txt",
|
||||
"compatibility.zip",
|
||||
] {
|
||||
if let Ok(token) = compute(path) {
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
if max_length.is_none() {
|
||||
tokens.push(format!("metadata:{}", " ".repeat(15)));
|
||||
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
|
||||
} else {
|
||||
tokens.push(compute(PATH_METADATA)?);
|
||||
tokens.push(compute(PATH_METADATA_PB)?);
|
||||
}
|
||||
|
||||
let mut joined = tokens.join(",");
|
||||
|
||||
if let Some(l) = max_length {
|
||||
if joined.len() > l {
|
||||
return Err(Error::InsufficientReservedSpace(joined, l));
|
||||
}
|
||||
|
||||
let remain = l - joined.len();
|
||||
joined.extend(iter::repeat(' ').take(remain));
|
||||
}
|
||||
|
||||
Ok(joined)
|
||||
}
|
||||
|
||||
// Add fake payload_metadata.bin entry, covering the header + header signature
|
||||
// regions of the payload.
|
||||
fn add_payload_metadata_entry(
|
||||
entries: &mut Vec<ZipEntry>,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<()> {
|
||||
let payload_offset = entries
|
||||
.iter()
|
||||
.find(|e| e.name == PATH_PAYLOAD)
|
||||
.ok_or_else(|| Error::MissingZipEntry(PATH_PAYLOAD))?
|
||||
.offset;
|
||||
entries.push(ZipEntry {
|
||||
name: NAME_PAYLOAD_METADATA.to_owned(),
|
||||
offset: payload_offset,
|
||||
size: payload_metadata_size,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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` 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.
|
||||
pub fn add_metadata(
|
||||
zip_entries: &[ZipEntry],
|
||||
zip_writer: &mut ZipWriter<impl Write>,
|
||||
next_offset: u64,
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<OtaMetadata> {
|
||||
let mut metadata = metadata.clone();
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
|
||||
let mut zip_entries = zip_entries.to_owned();
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
|
||||
// Compute initial property files with reserved space as placeholders to
|
||||
// store the self-referential metadata entries later.
|
||||
metadata.property_files.clear();
|
||||
for pf in [PF_NAME, PF_STREAMING_NAME] {
|
||||
metadata.property_files.insert(
|
||||
pf.to_owned(),
|
||||
compute_property_files(pf, &zip_entries, None)?,
|
||||
);
|
||||
}
|
||||
|
||||
// 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()));
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = writer.end_extra_data()?;
|
||||
writer.write_all(legacy_raw.as_bytes())?;
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA_PB, options)?;
|
||||
let modern_offset = writer.end_extra_data()?;
|
||||
writer.write_all(&modern_raw)?;
|
||||
|
||||
zip_entries.push(ZipEntry {
|
||||
name: PATH_METADATA.to_owned(),
|
||||
offset: next_offset + legacy_offset,
|
||||
size: legacy_raw.len() as u64,
|
||||
});
|
||||
zip_entries.push(ZipEntry {
|
||||
name: PATH_METADATA_PB.to_owned(),
|
||||
offset: next_offset + modern_offset,
|
||||
size: modern_raw.len() as u64,
|
||||
});
|
||||
|
||||
(next_offset + legacy_offset, next_offset + modern_offset)
|
||||
};
|
||||
|
||||
// Compute the final property files using the offsets of the fake entries.
|
||||
for (key, value) in &mut metadata.property_files {
|
||||
*value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
}
|
||||
|
||||
// Add the final metadata files to the real zip.
|
||||
{
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
|
||||
|
||||
zip_writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = zip_writer.end_extra_data()?;
|
||||
zip_writer.write_all(legacy_raw.as_bytes())?;
|
||||
|
||||
zip_writer.start_file_with_extra_data(PATH_METADATA_PB, options)?;
|
||||
let modern_offset = zip_writer.end_extra_data()?;
|
||||
zip_writer.write_all(&modern_raw)?;
|
||||
|
||||
assert_eq!(legacy_offset, temp_legacy_offset);
|
||||
assert_eq!(modern_offset, temp_modern_offset);
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Verify that the zip entry offsets and sizes match the OTA metadata.
|
||||
pub fn verify_metadata(
|
||||
reader: impl Read + Seek,
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<()> {
|
||||
let mut zip_reader = ZipArchive::new(reader)?;
|
||||
let mut zip_entries = vec![];
|
||||
|
||||
for i in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index(i)?;
|
||||
zip_entries.push(ZipEntry {
|
||||
name: entry.name().to_owned(),
|
||||
offset: entry.data_start(),
|
||||
size: entry.size(),
|
||||
});
|
||||
}
|
||||
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
|
||||
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 {
|
||||
expected: value.clone(),
|
||||
actual: new_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the CMS signature from the OTA zip comment. Returns the decoded CMS
|
||||
/// [`SignedData`] structure and the length of the file (from the beginning)
|
||||
/// that's covered by the signature. This does not perform any parsing of zip
|
||||
/// data structures.
|
||||
fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
let file_size = reader.seek(SeekFrom::End(0))?;
|
||||
|
||||
reader.seek(SeekFrom::Current(-6))?;
|
||||
let mut footer = [0u8; 6];
|
||||
reader.read_exact(&mut footer)?;
|
||||
|
||||
let abs_eoc_offset = u16::from_le_bytes(footer[0..2].try_into().unwrap());
|
||||
let sig_magic = u16::from_le_bytes(footer[2..4].try_into().unwrap());
|
||||
let comment_size = u16::from_le_bytes(footer[4..6].try_into().unwrap());
|
||||
|
||||
if sig_magic != 0xffff {
|
||||
return Err(Error::OtaMagicNotFound);
|
||||
}
|
||||
|
||||
// RecoverySystem.verifyPackage() always assumes a non-zip64 EOCD, so we'll
|
||||
// do the same.
|
||||
let eocd_size = u64::from(22 + comment_size);
|
||||
if file_size < eocd_size {
|
||||
return Err(Error::ZipTooSmall);
|
||||
} else if u64::from(abs_eoc_offset) > eocd_size {
|
||||
return Err(Error::SignatureOffsetTooLarge);
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Start(file_size - eocd_size))?;
|
||||
let mut eocd = vec![0u8; eocd_size as usize];
|
||||
reader.read_exact(&mut eocd)?;
|
||||
|
||||
let mut eocd_magic_iter = memmem::find_iter(&eocd, ZIP_EOCD_MAGIC);
|
||||
if eocd_magic_iter.next() != Some(0) {
|
||||
return Err(Error::EocdMagicNotFound);
|
||||
}
|
||||
if eocd_magic_iter.next().is_some() {
|
||||
return Err(Error::EocdMagicInComment);
|
||||
}
|
||||
|
||||
let sig_offset = eocd_size as usize - usize::from(abs_eoc_offset);
|
||||
let sd = crypto::parse_cms(&eocd[sig_offset..eocd_size as usize - 6])?;
|
||||
// The signature covers everything aside from the archive comment and its
|
||||
// length field.
|
||||
let hashed_size = file_size - 2 - u64::from(comment_size);
|
||||
|
||||
Ok((sd, hashed_size))
|
||||
}
|
||||
|
||||
/// Verify an OTA zip against its embedded certificates. This function makes no
|
||||
/// assertion about whether the certificate is actually trusted. Returns the
|
||||
/// embedded certificate.
|
||||
///
|
||||
/// 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: &AtomicBool) -> Result<Certificate> {
|
||||
let (sd, hashed_size) = parse_ota_sig(&mut reader)?;
|
||||
|
||||
// Make sure the certificate in the CMS structure matches the otacert zip
|
||||
// entry.
|
||||
let certs = crypto::get_cms_certs(&sd);
|
||||
if certs.len() != 1 {
|
||||
return Err(Error::NotOneCmsCertificate(certs.len()));
|
||||
}
|
||||
|
||||
let cert = &certs[0];
|
||||
let public_key = crypto::get_public_key(cert)?;
|
||||
|
||||
// Make sure this is a signature scheme we can handle. There's currently no
|
||||
// Rust library to verify arbitrary CMS signatures for large files without
|
||||
// fully reading them into memory.
|
||||
if sd.signer_infos.0.len() != 1 {
|
||||
return Err(Error::NotOneCmsSignerInfo(sd.signer_infos.0.len()));
|
||||
}
|
||||
|
||||
let signer = sd.signer_infos.0.get(0).unwrap();
|
||||
if signer.digest_alg.oid != rfc5912::ID_SHA_256 && signer.digest_alg.oid != rfc5912::ID_SHA_1 {
|
||||
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
|
||||
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
|
||||
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
|
||||
{
|
||||
return Err(Error::UnsupportedSignatureAlgorithm(
|
||||
signer.signature_algorithm.oid,
|
||||
));
|
||||
}
|
||||
|
||||
// Manually hash the parts of the file covered by the signature.
|
||||
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>())
|
||||
} else {
|
||||
(
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
Pkcs1v15Sign::new::<Sha1>(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut hashing_reader = HashingReader::new(reader, Context::new(algorithm));
|
||||
|
||||
stream::copy_n(&mut hashing_reader, io::sink(), hashed_size, cancel_signal)?;
|
||||
|
||||
let (_, context) = hashing_reader.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key.verify(scheme, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
|
||||
Ok(cert.clone())
|
||||
}
|
||||
|
||||
/// Get and parse the protobuf-encoded OTA metadata, the PEM-encoded otacert,
|
||||
/// the payload header, and the payload properties from an OTA zip.
|
||||
pub fn parse_zip_ota_info(
|
||||
reader: impl Read + Seek,
|
||||
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
|
||||
let mut zip = ZipArchive::new(reader)?;
|
||||
|
||||
let metadata = {
|
||||
let mut entry = zip.by_name(PATH_METADATA_PB)?;
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf)?;
|
||||
OtaMetadata::decode(buf.as_slice())?
|
||||
};
|
||||
|
||||
let certificate = {
|
||||
let entry = zip.by_name(PATH_OTACERT)?;
|
||||
crypto::read_pem_cert(entry)?
|
||||
};
|
||||
|
||||
let header = {
|
||||
let entry = zip.by_name(PATH_PAYLOAD)?;
|
||||
PayloadHeader::from_reader(entry)?
|
||||
};
|
||||
|
||||
let properties = {
|
||||
let mut entry = zip.by_name(PATH_PROPERTIES)?;
|
||||
let mut buf = String::new();
|
||||
entry.read_to_string(&mut buf)?;
|
||||
buf
|
||||
};
|
||||
|
||||
Ok((metadata, certificate, header, properties))
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
inner: HashingWriter<W>,
|
||||
// Android only supports non-zip64 EOCD.
|
||||
queue: [u8; 22],
|
||||
used: usize,
|
||||
}
|
||||
|
||||
impl<W: Write> SigningWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner: HashingWriter::new(inner, Context::new(&ring::digest::SHA256)),
|
||||
queue: Default::default(),
|
||||
used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self, key: &RsaPrivateKey, 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());
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
|
||||
Ok(raw_writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SigningWriter<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() {
|
||||
// Write data from the front of the queue while keeping it as full
|
||||
// as possible.
|
||||
let n_from_queue = (self.used + back.len()).saturating_sub(self.queue.len());
|
||||
self.inner.write_all(&self.queue[..n_from_queue])?;
|
||||
|
||||
// Move unused queued bytes to the front.
|
||||
self.queue.rotate_left(n_from_queue);
|
||||
self.used -= n_from_queue;
|
||||
|
||||
// Add the remaining data to the queue.
|
||||
self.queue[self.used..self.used + back.len()].copy_from_slice(back);
|
||||
self.used += back.len();
|
||||
} else {
|
||||
// We have enough data in the back to fill the entire queue.
|
||||
self.inner.write_all(&self.queue[..self.used])?;
|
||||
self.inner.write_all(front)?;
|
||||
|
||||
self.queue.copy_from_slice(back);
|
||||
self.used = self.queue.len();
|
||||
}
|
||||
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use num_traits::PrimInt;
|
||||
|
||||
use crate::stream::{ReadDiscardExt, WriteZerosExt};
|
||||
|
||||
/// Calculate the amount of padding that needs to be added to align the
|
||||
/// specified offset to a page boundary.
|
||||
pub fn calc<N: PrimInt>(offset: N, page_size: N) -> N {
|
||||
let r = offset % page_size;
|
||||
if r == N::zero() {
|
||||
N::zero()
|
||||
} else {
|
||||
page_size - r
|
||||
}
|
||||
}
|
||||
|
||||
/// Round to the next multiple of the page size.
|
||||
pub fn round<N: PrimInt>(offset: N, page_size: N) -> Option<N> {
|
||||
let remain = calc(offset, page_size);
|
||||
offset.checked_add(&remain)
|
||||
}
|
||||
|
||||
/// Read and discard data until the next multiple of the page size. [`Seek`] is
|
||||
/// only used for querying the file position.
|
||||
pub fn read_discard(mut reader: impl Read + Seek, page_size: u64) -> io::Result<u64> {
|
||||
let pos = reader.stream_position()?;
|
||||
let padding = calc(pos, page_size);
|
||||
|
||||
reader.read_discard_exact(padding)?;
|
||||
|
||||
Ok(padding)
|
||||
}
|
||||
|
||||
/// Write zeros until the next multiple of the page size. [`Seek`] is only used
|
||||
/// for querying the file position.
|
||||
pub fn write_zeros(mut writer: impl Write + Seek, page_size: u64) -> io::Result<u64> {
|
||||
let pos = writer.stream_position()?;
|
||||
let padding = calc(pos, page_size);
|
||||
|
||||
writer.write_zeros_exact(padding)?;
|
||||
|
||||
Ok(padding)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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(),
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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
|
||||
//! in patch releases.
|
||||
//!
|
||||
//! The CLI source files use concrete types wherever possible for simplicity,
|
||||
//! while the "library"-style source files aim to be generic.
|
||||
|
||||
// We use pb-rs' nostd mode. See build.rs.
|
||||
extern crate alloc;
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn main() {
|
||||
// Set up a cancel signal so we can properly clean up any temporary files.
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
let signal = cancel_signal.clone();
|
||||
|
||||
ctrlc::set_handler(move || {
|
||||
signal.store(true, Ordering::SeqCst);
|
||||
})
|
||||
.expect("Failed to set signal handler");
|
||||
}
|
||||
|
||||
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
|
||||
error!("{e:?}");
|
||||
} else {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
pub mod boot;
|
||||
pub mod otacert;
|
||||
pub mod system;
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use x509_cert::{der::asn1::BitString, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
|
||||
|
||||
use crate::{crypto, format::ota};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("New otacerts.zip is too small to pad to {0} bytes")]
|
||||
ZipTooSmall(usize),
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Pad a non-zip64 zip file to the specified size by adding null bytes to the
|
||||
/// archive comment field.
|
||||
pub fn pad_zip(data: &mut Vec<u8>, size: usize) -> Result<()> {
|
||||
match size.cmp(&data.len()) {
|
||||
Ordering::Equal => Ok(()),
|
||||
Ordering::Less => Err(Error::ZipTooLarge(size)),
|
||||
Ordering::Greater => {
|
||||
let padding = size - data.len();
|
||||
|
||||
if data.len() < 22
|
||||
|| &data[data.len() - 22..][..4] != ota::ZIP_EOCD_MAGIC
|
||||
|| padding > usize::from(u16::MAX)
|
||||
{
|
||||
return Err(Error::ZipTooSmall(size));
|
||||
}
|
||||
|
||||
// Rewrite the comment size and pad with null bytes.
|
||||
data.pop();
|
||||
data.pop();
|
||||
data.extend((padding as u16).to_le_bytes());
|
||||
data.resize(size, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Android uses X.509 as nothing more than a file format to transport RSA
|
||||
/// public keys. This is true for both the framework's RecoverySystem and
|
||||
/// recovery's otautil/verifier.cpp. The only fields that must exist are
|
||||
/// the public key and the signature algorithm. The rest can be removed with
|
||||
/// no side effects whatsoever.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct OtaCertBuildFlags: u8 {
|
||||
const COMPRESS_DEFLATE = 1 << 0;
|
||||
const REMOVE_SIGNATURE = 1 << 1;
|
||||
const REMOVE_EXTENSIONS = 1 << 2;
|
||||
const REMOVE_ISSUER = 1 << 3;
|
||||
const REMOVE_SUBJECT = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an `otacerts.zip` file containing the specified certificate.
|
||||
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
|
||||
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Stored
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
} else {
|
||||
let mut modified = cert.clone();
|
||||
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
|
||||
modified.signature = BitString::from_bytes(&[])?;
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
|
||||
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
|
||||
extensions.clear();
|
||||
}
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
|
||||
modified.tbs_certificate.issuer.0.clear();
|
||||
modified.tbs_certificate.issuer_unique_id = None;
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SUBJECT) {
|
||||
modified.tbs_certificate.subject.0.clear();
|
||||
modified.tbs_certificate.subject_unique_id = None;
|
||||
}
|
||||
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
/// Create an `otacerts.zip` file padded to the specified size.
|
||||
///
|
||||
/// This will incrementally remove unneeded components from the certificate to
|
||||
/// meet the size limit if needed.
|
||||
pub fn create_zip_with_size(cert: &Certificate, size: usize) -> Result<Vec<u8>> {
|
||||
let mut flags = OtaCertBuildFlags::empty();
|
||||
|
||||
for additional_flag in [
|
||||
OtaCertBuildFlags::empty(),
|
||||
OtaCertBuildFlags::COMPRESS_DEFLATE,
|
||||
OtaCertBuildFlags::REMOVE_SIGNATURE,
|
||||
OtaCertBuildFlags::REMOVE_EXTENSIONS,
|
||||
OtaCertBuildFlags::REMOVE_ISSUER,
|
||||
OtaCertBuildFlags::REMOVE_SUBJECT,
|
||||
] {
|
||||
flags |= additional_flag;
|
||||
|
||||
trace!("Attempting to create {size} byte otacerts.zip: {flags:?}");
|
||||
|
||||
let mut data = create_zip(cert, flags)?;
|
||||
if data.len() <= size {
|
||||
trace!("Padding {} byte otacerts.zip to {size}", data.len());
|
||||
|
||||
pad_zip(&mut data, size)?;
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::ZipTooLarge(size))
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, SeekFrom},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use rsa::RsaPrivateKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, Span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer},
|
||||
ota,
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Old otacerts.zip not found in image")]
|
||||
OldZipNotFound,
|
||||
#[error("Image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash tree descriptor found in vbmeta header")]
|
||||
NoHashTreeDescriptor,
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Find the bounds of a non-zip64 zip starting from the EOCD magic offset.
|
||||
fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
|
||||
let eocd = &data[eocd_offset..];
|
||||
if eocd.len() < 22 {
|
||||
trace!("Buffer is too small to contain EOCD");
|
||||
return None;
|
||||
}
|
||||
|
||||
let cd_size = u32::from_le_bytes(eocd[12..16].try_into().unwrap()) as usize;
|
||||
let cd_offset = u32::from_le_bytes(eocd[16..20].try_into().unwrap()) as usize;
|
||||
let comment_size = usize::from(u16::from_le_bytes(eocd[20..22].try_into().unwrap()));
|
||||
|
||||
let start = eocd_offset.checked_sub(cd_size)?.checked_sub(cd_offset)?;
|
||||
let end = eocd_offset.checked_add(22)?.checked_add(comment_size)?;
|
||||
if end > data.len() {
|
||||
trace!("End of zip is out of bounds");
|
||||
return None;
|
||||
}
|
||||
|
||||
trace!("Found zip bounds: {:?}", start..end);
|
||||
|
||||
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
|
||||
let mut zip_reader = ZipArchive::new(reader).ok()?;
|
||||
|
||||
if zip_reader.is_empty() {
|
||||
// otacerts.zip files contain at least one cert.
|
||||
trace!("Zip is empty");
|
||||
return None;
|
||||
}
|
||||
|
||||
for index in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index_raw(index).ok()?;
|
||||
|
||||
if !entry.name().ends_with(".x509.pem") {
|
||||
// otacerts.zip files only contain files named this way.
|
||||
trace!("Excluded due to invalid name: {:?}", entry.name());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Found otacerts.zip candidate");
|
||||
|
||||
// There's one or more entries and every one is named *.x509.pem.
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
/// Replace `otacerts.zip` with a new one containing the new certificate, but
|
||||
/// padded to the same size. If the new zip is too large, the certificate will
|
||||
/// be modified to remove unnecessary components until it fits. All operations
|
||||
/// run in parallel where possible. The input and output must refer to the same
|
||||
/// file and will be reopened from multiple threads.
|
||||
///
|
||||
/// Returns two sorted and non-overlapping lists of byte ranges that were
|
||||
/// modified. The first list are the byte regions within the filesystem data
|
||||
/// that contained otacerts.zip. The second list is the list of byte regions
|
||||
/// outside of the filesyste, like the hash tree, FEC data, and AVB metadata.
|
||||
///
|
||||
/// If [`Error::OldZipNotFound`] is returned, the output will not have been
|
||||
/// modified.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn patch_system_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
certificate: &Certificate,
|
||||
key: &RsaPrivateKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
|
||||
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
|
||||
// This ensures that the block containing otacerts.zip's data won't cross
|
||||
// chunk boundaries.
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
let num_chunks = 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, 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))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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"));
|
||||
}
|
||||
@@ -0,0 +1,991 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
use crate::util;
|
||||
|
||||
/// A trait for seekable readers. This is only needed because `dyn Read + Seek`
|
||||
/// is not a valid construct in Rust yet.
|
||||
pub trait ReadSeek: Read + Seek {}
|
||||
|
||||
impl<R: Read + Seek> ReadSeek for R {}
|
||||
|
||||
/// A trait for seekable writers. This is only needed because `dyn Write + Seek`
|
||||
/// is not a valid construct in Rust yet.
|
||||
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;
|
||||
|
||||
fn from_reader(reader: R) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
/// Common function for writing a structure to a writer.
|
||||
pub trait ToWriter<W: Write>: Sized {
|
||||
type Error;
|
||||
|
||||
fn to_writer(&self, writer: W) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Extensions for readers to read and discard data (eg. for padding).
|
||||
pub trait ReadDiscardExt {
|
||||
fn read_discard(&mut self, size: u64) -> io::Result<u64>;
|
||||
|
||||
fn read_discard_exact(&mut self, size: u64) -> io::Result<()> {
|
||||
let n = self.read_discard(size)?;
|
||||
if n != size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("Expected to read {size} bytes, but reached EOF after {n} bytes"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> ReadDiscardExt for R {
|
||||
fn read_discard(&mut self, size: u64) -> io::Result<u64> {
|
||||
io::copy(&mut self.take(size), &mut io::sink())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for writers to easily write zeros (eg. for padding).
|
||||
pub trait WriteZerosExt {
|
||||
fn write_zeros(&mut self, size: u64) -> io::Result<u64>;
|
||||
|
||||
fn write_zeros_exact(&mut self, size: u64) -> io::Result<()> {
|
||||
let n = self.write_zeros(size)?;
|
||||
if n != size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("Expected to write {size} bytes, but reached EOF after {n} bytes"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> WriteZerosExt for W {
|
||||
fn write_zeros(&mut self, size: u64) -> io::Result<u64> {
|
||||
// We don't use std::io::copy() on std::io::repeat(0) because it fails
|
||||
// if the writer hits EOF before all data is written.
|
||||
let mut written = 0;
|
||||
|
||||
while written < size {
|
||||
let to_write = (size - written).min(util::ZEROS.len() as u64) as usize;
|
||||
let n = self.write(&util::ZEROS[..to_write])?;
|
||||
written += n as u64;
|
||||
|
||||
if n < to_write {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for readers to read strings.
|
||||
pub trait ReadStringExt {
|
||||
/// Read exact sized string.
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String>;
|
||||
|
||||
/// Read string with maximum size and trim trailing zeros.
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String>;
|
||||
}
|
||||
|
||||
impl<R: Read> ReadStringExt for R {
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String> {
|
||||
let mut buf = vec![0u8; size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String> {
|
||||
let mut buf = vec![0u8; max_size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
let after_last_non_zero = buf
|
||||
.iter()
|
||||
.rev()
|
||||
.position(|&b| b != 0)
|
||||
.map_or(0, |i| buf.len() - i);
|
||||
buf.resize(after_last_non_zero, 0);
|
||||
buf.shrink_to_fit();
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for writers to write strings.
|
||||
pub trait WriteStringExt {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl<W: Write> WriteStringExt for W {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()> {
|
||||
if data.len() > max_size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{data:?} exceeds maximum size of {max_size} bytes"),
|
||||
));
|
||||
}
|
||||
|
||||
self.write_all(data.as_bytes())?;
|
||||
|
||||
let num_zeros = (max_size - data.len()) as u64;
|
||||
self.write_zeros_exact(num_zeros)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
inner: R,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl<R: Read> CountingReader<R> {
|
||||
pub fn new(inner: R) -> Self {
|
||||
Self { inner, offset: 0 }
|
||||
}
|
||||
|
||||
pub fn finish(self) -> (R, u64) {
|
||||
(self.inner, self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for CountingReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let n = self.inner.read(buf)?;
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Seek for CountingReader<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
if pos == SeekFrom::Current(0) {
|
||||
Ok(self.offset)
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Can only report current offset",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer wrapper that implements [`Seek`], but only for reporting the
|
||||
/// current file position.
|
||||
pub struct CountingWriter<W: Write> {
|
||||
inner: W,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl<W: Write> CountingWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self { inner, offset: 0 }
|
||||
}
|
||||
|
||||
pub fn finish(self) -> (W, u64) {
|
||||
(self.inner, self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for CountingWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.inner.write(buf)?;
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Seek for CountingWriter<W> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
if pos == SeekFrom::Current(0) {
|
||||
Ok(self.offset)
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Can only report current offset",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A reader wrapper that hashes data as it's being read.
|
||||
pub struct HashingReader<R: Read> {
|
||||
inner: R,
|
||||
context: Context,
|
||||
}
|
||||
|
||||
impl<R: Read> HashingReader<R> {
|
||||
pub fn new(inner: R, context: Context) -> Self {
|
||||
Self { inner, context }
|
||||
}
|
||||
|
||||
pub fn finish(self) -> (R, Context) {
|
||||
(self.inner, self.context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for HashingReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let n = self.inner.read(buf)?;
|
||||
self.context.update(&buf[..n]);
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer wrapper that hashes data as it's being written.
|
||||
pub struct HashingWriter<W: Write> {
|
||||
inner: W,
|
||||
context: Context,
|
||||
}
|
||||
|
||||
impl<W: Write> HashingWriter<W> {
|
||||
pub fn new(inner: W, context: Context) -> Self {
|
||||
Self { inner, context }
|
||||
}
|
||||
|
||||
pub fn finish(self) -> (W, Context) {
|
||||
(self.inner, self.context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for HashingWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.inner.write(buf)?;
|
||||
self.context.update(&buf[..n]);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A reader wrapper that only allows reading a specific section of a file.
|
||||
pub struct SectionReader<R: Read + Seek> {
|
||||
inner: R,
|
||||
start: u64,
|
||||
size: u64,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> SectionReader<R> {
|
||||
pub fn new(mut inner: R, start: u64, size: u64) -> io::Result<Self> {
|
||||
inner.seek(SeekFrom::Start(start))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
start,
|
||||
size,
|
||||
pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
let n = self.inner.read(&mut buf[..to_read])?;
|
||||
self.pos += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for SectionReader<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.pos = match pos {
|
||||
SeekFrom::Start(o) => o,
|
||||
SeekFrom::End(o) => self
|
||||
.size
|
||||
.to_i64()
|
||||
.and_then(|s| s.checked_add(o))
|
||||
.and_then(|s| s.to_u64())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Offset would be before the start of the file",
|
||||
)
|
||||
})?,
|
||||
SeekFrom::Current(o) => self
|
||||
.pos
|
||||
.to_i64()
|
||||
.and_then(|s| s.checked_add(o))
|
||||
.and_then(|s| s.to_u64())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Offset would be before the start of the file",
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let raw_pos = self.inner.seek(SeekFrom::Start(self.start + self.pos))?;
|
||||
Ok(raw_pos - self.start)
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer wrapper that seeks instead of writing when a write buffer consists
|
||||
/// solely of zeros.
|
||||
#[derive(Debug)]
|
||||
pub struct HolePunchingWriter<W: Write + Seek> {
|
||||
inner: W,
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> HolePunchingWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> W {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> Write for HolePunchingWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
if util::is_zero(buf) {
|
||||
self.inner.seek(SeekFrom::Current(buf.len() as i64))?;
|
||||
Ok(buf.len())
|
||||
} else {
|
||||
self.inner.write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file wrapper that uses a userspace file offset. A reopened instance uses
|
||||
/// the same underlying kernel file descriptor, but a new userspace file offset,
|
||||
/// initially set to 0.
|
||||
#[derive(Debug)]
|
||||
pub struct PSeekFile {
|
||||
// The lock is needed because flush() takes a `&mut self`.
|
||||
file: Arc<RwLock<File>>,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl PSeekFile {
|
||||
pub fn new(file: File) -> Self {
|
||||
Self {
|
||||
file: Arc::new(RwLock::new(file)),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_len(&self, size: u64) -> io::Result<()> {
|
||||
let file_locked = self.file.read().unwrap();
|
||||
file_locked.set_len(size)
|
||||
}
|
||||
|
||||
/// Read data from offset. The kernel's file position *will* be changed.
|
||||
#[cfg(windows)]
|
||||
fn read_at(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
use std::os::windows::fs::FileExt;
|
||||
self.file.read().unwrap().seek_read(buf, self.offset)
|
||||
}
|
||||
|
||||
/// Read data from offset. The kernel's file position will *not* be changed.
|
||||
#[cfg(unix)]
|
||||
fn read_at(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
use std::os::unix::fs::FileExt;
|
||||
self.file.read().unwrap().read_at(buf, self.offset)
|
||||
}
|
||||
|
||||
/// Write data to offset. The kernel's file position *will* be changed.
|
||||
#[cfg(windows)]
|
||||
fn write_at(&self, buf: &[u8]) -> io::Result<usize> {
|
||||
use std::os::windows::fs::FileExt;
|
||||
self.file.read().unwrap().seek_write(buf, self.offset)
|
||||
}
|
||||
|
||||
/// Write data to offset. The kernel's file position will *not* be changed.
|
||||
#[cfg(unix)]
|
||||
fn write_at(&self, buf: &[u8]) -> io::Result<usize> {
|
||||
use std::os::unix::fs::FileExt;
|
||||
self.file.read().unwrap().write_at(buf, self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for PSeekFile {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.write_at(buf)?;
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.file.write().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for PSeekFile {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.offset = match pos {
|
||||
SeekFrom::Start(o) => o,
|
||||
SeekFrom::End(o) => {
|
||||
let file_size = self.file.read().unwrap().metadata()?.len();
|
||||
file_size
|
||||
.to_i64()
|
||||
.and_then(|s| s.checked_add(o))
|
||||
.and_then(|s| s.to_u64())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Offset would be before the start of the file",
|
||||
)
|
||||
})?
|
||||
}
|
||||
SeekFrom::Current(o) => self
|
||||
.offset
|
||||
.to_i64()
|
||||
.and_then(|s| s.checked_add(o))
|
||||
.and_then(|s| s.to_u64())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Offset would be before the start of the file",
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
Ok(self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// A small wrapper around a [`Cursor`] that allows multiple instances to share
|
||||
/// the same underlying file. All reads, writes, and seeks are single-threaded.
|
||||
/// This is useful for scenarios where data needs to be copied from multiple
|
||||
/// 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(Default)]
|
||||
pub struct SharedCursor {
|
||||
inner: Arc<Mutex<Cursor<Vec<u8>>>>,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl SharedCursor {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for SharedCursor {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.seek(SeekFrom::Start(self.offset))?;
|
||||
|
||||
let n = inner.read(buf)?;
|
||||
self.offset += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SharedCursor {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.seek(SeekFrom::Start(self.offset))?;
|
||||
|
||||
let n = inner.write(buf)?;
|
||||
self.offset += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for SharedCursor {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
self.offset = inner.seek(pos)?;
|
||||
Ok(self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// cancelled on the next loop iteration if `cancel_signal` is set to `true`.
|
||||
pub fn copy_n_inspect(
|
||||
mut reader: impl Read,
|
||||
mut writer: impl Write,
|
||||
mut size: u64,
|
||||
mut inspect: impl FnMut(&[u8]),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
let mut buf = [0u8; 16384];
|
||||
|
||||
while size > 0 {
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
let to_read = size.min(buf.len() as u64) as usize;
|
||||
reader.read_exact(&mut buf[..to_read])?;
|
||||
|
||||
inspect(&buf[..to_read]);
|
||||
|
||||
writer.write_all(&buf[..to_read])?;
|
||||
|
||||
size -= to_read as u64;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy exactly `size` bytes from `reader` to `writer`.
|
||||
pub fn copy_n(
|
||||
reader: impl Read,
|
||||
writer: impl Write,
|
||||
size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
copy_n_inspect(reader, writer, size, |_| {}, cancel_signal)
|
||||
}
|
||||
|
||||
/// Copy data from `reader` to `writer` until `reader` reaches EOF. If `writer`
|
||||
/// reaches EOF before `reader` does, an error is returned. The operation is
|
||||
/// cancelled on the next loop iteration if `cancel_signal` is set to `true`.
|
||||
pub fn copy(
|
||||
mut reader: impl Read,
|
||||
mut writer: impl Write,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<u64> {
|
||||
let mut buf = [0u8; 16384];
|
||||
let mut copied = 0;
|
||||
|
||||
loop {
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
let n = reader.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
writer.write_all(&buf[..n])?;
|
||||
|
||||
copied += n as u64;
|
||||
}
|
||||
|
||||
Ok(copied)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use ring::digest::Context;
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, HolePunchingWriter,
|
||||
PSeekFile, ReadDiscardExt, ReadStringExt, Reopen, SectionReader, SharedCursor,
|
||||
WriteStringExt, WriteZerosExt,
|
||||
};
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
0xc3, 0xab, 0x8f, 0xf1, 0x37, 0x20, 0xe8, 0xad, 0x90, 0x47, 0xdd, 0x39, 0x46, 0x6b, 0x3c,
|
||||
0x89, 0x74, 0xe5, 0x92, 0xc2, 0xfa, 0x38, 0x3d, 0x4a, 0x39, 0x60, 0x71, 0x4c, 0xae, 0xf0,
|
||||
0xc4, 0xf2,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn read_discard() {
|
||||
let mut reader = Cursor::new(b"foobar");
|
||||
reader.read_discard_exact(3).unwrap();
|
||||
|
||||
let mut buf = [0u8; 2];
|
||||
reader.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"ba");
|
||||
|
||||
let n = reader.read_discard(2).unwrap();
|
||||
assert_eq!(n, 1);
|
||||
|
||||
assert_eq!(reader.stream_position().unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_zeros() {
|
||||
let mut writer = Cursor::new([0u8; 6]);
|
||||
|
||||
writer.write_zeros_exact(2).unwrap();
|
||||
writer.write_all(b"foo").unwrap();
|
||||
|
||||
let n = writer.write_zeros(2).unwrap();
|
||||
assert_eq!(n, 1);
|
||||
|
||||
assert_eq!(&writer.into_inner(), b"\0\0foo\0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_string() {
|
||||
let mut reader = Cursor::new(b"foo\0\0bar\0\0");
|
||||
|
||||
assert_eq!(reader.read_string_exact(3).unwrap(), "foo");
|
||||
assert_eq!(reader.read_string_exact(0).unwrap(), "");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(3).unwrap(), "foo");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(10).unwrap(), "foo\0\0bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_string() {
|
||||
let mut writer = Cursor::new([0xffu8; 8]);
|
||||
|
||||
writer.write_string_padded("foobar", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobar\0\0");
|
||||
|
||||
writer.rewind().unwrap();
|
||||
writer.write_string_padded("foobarhi", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobarhi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counting_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
let mut reader = CountingReader::new(raw_reader);
|
||||
|
||||
let mut buf = [0u8; 6];
|
||||
reader.read_exact(&mut buf[..0]).unwrap();
|
||||
reader.read_exact(&mut buf[..3]).unwrap();
|
||||
reader.read_exact(&mut buf[3..4]).unwrap();
|
||||
reader.read_exact(&mut buf[4..6]).unwrap();
|
||||
assert_eq!(&buf, b"foobar");
|
||||
|
||||
let (mut raw_reader, size) = reader.finish();
|
||||
assert_eq!(raw_reader.stream_position().unwrap(), 6);
|
||||
assert_eq!(size, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counting_writer() {
|
||||
let raw_writer = Cursor::new([0u8; 6]);
|
||||
let mut writer = CountingWriter::new(raw_writer);
|
||||
|
||||
writer.write_all(b"foo").unwrap();
|
||||
writer.write_all(b"").unwrap();
|
||||
writer.write_all(b"bar").unwrap();
|
||||
|
||||
let (mut raw_writer, size) = writer.finish();
|
||||
assert_eq!(raw_writer.stream_position().unwrap(), 6);
|
||||
assert_eq!(&raw_writer.into_inner(), b"foobar");
|
||||
assert_eq!(size, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashing_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
let mut reader = HashingReader::new(raw_reader, Context::new(&ring::digest::SHA256));
|
||||
|
||||
let mut buf = [0u8; 6];
|
||||
reader.read_exact(&mut buf[..0]).unwrap();
|
||||
reader.read_exact(&mut buf[..3]).unwrap();
|
||||
reader.read_exact(&mut buf[3..4]).unwrap();
|
||||
reader.read_exact(&mut buf[4..6]).unwrap();
|
||||
assert_eq!(&buf, b"foobar");
|
||||
|
||||
let (mut raw_reader, context) = reader.finish();
|
||||
assert_eq!(raw_reader.stream_position().unwrap(), 6);
|
||||
assert_eq!(context.finish().as_ref(), FOOBAR_SHA256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashing_writer() {
|
||||
let raw_writer = Cursor::new([0u8; 6]);
|
||||
let mut writer = HashingWriter::new(raw_writer, Context::new(&ring::digest::SHA256));
|
||||
|
||||
writer.write_all(b"").unwrap();
|
||||
writer.write_all(b"foo").unwrap();
|
||||
writer.write_all(b"bar").unwrap();
|
||||
|
||||
let (mut raw_writer, context) = writer.finish();
|
||||
assert_eq!(raw_writer.stream_position().unwrap(), 6);
|
||||
assert_eq!(&raw_writer.into_inner(), b"foobar");
|
||||
assert_eq!(context.finish().as_ref(), FOOBAR_SHA256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_reader() {
|
||||
let raw_reader = Cursor::new(b"fooinnerbar");
|
||||
let mut reader = SectionReader::new(raw_reader, 3, 5).unwrap();
|
||||
|
||||
let mut buf = [0u8; 5];
|
||||
reader.read_exact(&mut buf[..0]).unwrap();
|
||||
reader.read_exact(&mut buf[..3]).unwrap();
|
||||
reader.read_exact(&mut buf[3..5]).unwrap();
|
||||
assert_eq!(&buf, b"inner");
|
||||
|
||||
let n = reader.read_discard(1).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
|
||||
buf = *b"\0\0\0\0\0";
|
||||
reader.seek(SeekFrom::Start(4)).unwrap();
|
||||
reader.read_exact(&mut buf[..1]).unwrap();
|
||||
assert_eq!(&buf[..1], b"r");
|
||||
|
||||
buf = *b"\0\0\0\0\0";
|
||||
reader.seek(SeekFrom::End(-4)).unwrap();
|
||||
reader.read_exact(&mut buf[..4]).unwrap();
|
||||
assert_eq!(&buf[..4], b"nner");
|
||||
|
||||
buf = *b"\0\0\0\0\0";
|
||||
reader.seek(SeekFrom::Current(-5)).unwrap();
|
||||
reader.read_exact(&mut buf[..3]).unwrap();
|
||||
assert_eq!(&buf[..3], b"inn");
|
||||
|
||||
let mut raw_reader = reader.into_inner();
|
||||
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.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
b.write_all(b"world").unwrap();
|
||||
c.seek(SeekFrom::Start(0)).unwrap();
|
||||
c.write_all(b"hi").unwrap();
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
a.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"hillorworld");
|
||||
|
||||
let n = a.read_discard(1).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_cursor() {
|
||||
let mut a = SharedCursor::default();
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
b.write_all(b"world").unwrap();
|
||||
c.seek(SeekFrom::Start(0)).unwrap();
|
||||
c.write_all(b"hi").unwrap();
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
a.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"hillorworld");
|
||||
|
||||
let n = a.read_discard(1).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let mut reader = Cursor::new(b"foobar");
|
||||
let mut writer = Cursor::new([0u8; 6]);
|
||||
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobar");
|
||||
|
||||
// Reader early EOF.
|
||||
reader.seek(SeekFrom::Start(3)).unwrap();
|
||||
writer.rewind().unwrap();
|
||||
let err =
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
|
||||
|
||||
// Writer early EOF.
|
||||
reader.rewind().unwrap();
|
||||
writer.seek(SeekFrom::Start(3)).unwrap();
|
||||
let err =
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
|
||||
|
||||
reader.rewind().unwrap();
|
||||
writer.rewind().unwrap();
|
||||
let n = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap();
|
||||
assert_eq!(n, 6);
|
||||
assert_eq!(writer.get_ref(), b"foobar");
|
||||
|
||||
// Reader early EOF.
|
||||
reader.seek(SeekFrom::Start(3)).unwrap();
|
||||
writer.rewind().unwrap();
|
||||
let n = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap();
|
||||
assert_eq!(n, 3);
|
||||
|
||||
// Writer early EOF.
|
||||
reader.rewind().unwrap();
|
||||
writer.seek(SeekFrom::Start(3)).unwrap();
|
||||
let err = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
|
||||
|
||||
reader.rewind().unwrap();
|
||||
writer.rewind().unwrap();
|
||||
cancel_signal.store(true, Ordering::SeqCst);
|
||||
let err =
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Interrupted);
|
||||
let err = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Interrupted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
|
||||
|
||||
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<T: PrimInt>(pub T);
|
||||
|
||||
impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.0 == T::one() {
|
||||
write!(f, "<{:?} byte>", self.0)
|
||||
} else {
|
||||
write!(f, "<{:?} bytes>", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a byte slice is all zeros.
|
||||
pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
while !buf.is_empty() {
|
||||
let n = buf.len().min(ZEROS.len());
|
||||
if buf[..n] != ZEROS[..n] {
|
||||
return false;
|
||||
}
|
||||
|
||||
buf = &buf[n..];
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// 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(".")
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
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::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
|
||||
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
|
||||
KernelCmdlineDescriptor, PropertyDescriptor,
|
||||
},
|
||||
stream::SharedCursor,
|
||||
};
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
let data = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
|
||||
));
|
||||
let passphrase = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
|
||||
));
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
}
|
||||
|
||||
fn repeat_str(s: &str, max_len: usize) -> String {
|
||||
assert!(!s.is_empty());
|
||||
|
||||
let mut result = s.repeat(max_len / s.len());
|
||||
result.push_str(&s[..max_len % s.len()]);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn repeat_array<const N: usize>(data: &[u8]) -> [u8; N] {
|
||||
assert!(!data.is_empty());
|
||||
|
||||
let mut result = [0u8; N];
|
||||
|
||||
for i in 0..N / data.len() {
|
||||
result[i * data.len()..][..data.len()].copy_from_slice(data);
|
||||
}
|
||||
|
||||
let remain = N % data.len();
|
||||
result[N - remain..].copy_from_slice(&data[..remain]);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_root_image() {
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: 4096,
|
||||
tree_offset: 0,
|
||||
tree_size: 2048,
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 1,
|
||||
fec_offset: 2048,
|
||||
fec_size: 2048,
|
||||
hash_algorithm: "sha512".to_owned(),
|
||||
partition_name: "hashtreed_partition".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(8),
|
||||
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(8),
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
Descriptor::Hash(HashDescriptor {
|
||||
image_size: 6,
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "hashed_partition".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(4),
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
Descriptor::KernelCmdline(KernelCmdlineDescriptor {
|
||||
flags: 1,
|
||||
cmdline: "foobar".to_owned(),
|
||||
}),
|
||||
Descriptor::ChainPartition(ChainPartitionDescriptor {
|
||||
rollback_index_location: 1,
|
||||
partition_name: "chained_partition".to_owned(),
|
||||
public_key: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(129),
|
||||
flags: 0xfedcba98,
|
||||
reserved: repeat_array(&[0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
// Sign the header.
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
avb::write_root_image(&mut writer, &header, 64).unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
|
||||
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
|
||||
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
|
||||
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
|
||||
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
assert_matches!(new_footer, None);
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_image_size, data.len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_image() {
|
||||
let 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);
|
||||
|
||||
// Write the raw partition data.
|
||||
writer.write_all(raw_data).unwrap();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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, 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() {
|
||||
let image_size = 28672;
|
||||
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, 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(),
|
||||
[
|
||||
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, image_size);
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
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 {
|
||||
let data = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
|
||||
));
|
||||
let passphrase = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
|
||||
));
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).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);
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
image.to_writer(&mut writer).unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
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 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 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 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 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 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 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 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 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);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io::{Cursor, Read, Seek, Write};
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::compression::{CompressedFormat, CompressedReader, CompressedWriter},
|
||||
};
|
||||
|
||||
fn round_trip(data: &[u8], format: CompressedFormat) {
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = CompressedWriter::new(raw_writer, format).unwrap();
|
||||
writer.write_all(data).unwrap();
|
||||
let mut raw_reader = writer.finish().unwrap();
|
||||
|
||||
raw_reader.rewind().unwrap();
|
||||
let mut reader = CompressedReader::new(raw_reader, false).unwrap();
|
||||
assert_eq!(reader.format(), format);
|
||||
|
||||
let mut new_data = vec![];
|
||||
reader.read_to_end(&mut new_data).unwrap();
|
||||
|
||||
assert_eq!(data, new_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_gzip() {
|
||||
round_trip(b"gzip-compressed data", CompressedFormat::Gzip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_lz4_legacy() {
|
||||
// Make sure we exceed the 8MiB block boundary.
|
||||
let data = b"Lz4Legacy".repeat(1024 * 1024);
|
||||
round_trip(&data, CompressedFormat::Lz4Legacy);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
_ZERO_BLOCK = memoryview(b'\0' * 16384)
|
||||
|
||||
umask = None
|
||||
|
||||
|
||||
def load_umask_unsafe():
|
||||
# POSIX provides no way to query the umask without changing it. Parsing
|
||||
# /proc/self/status can work, but it's Linux only. Instead, we'll just do it
|
||||
# once when the program is initially started.
|
||||
global umask
|
||||
|
||||
if os.name != 'nt' and umask is None:
|
||||
current_umask = os.umask(0o777)
|
||||
os.umask(current_umask)
|
||||
|
||||
umask = current_umask
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@functools.total_ordering
|
||||
class Range:
|
||||
'''
|
||||
Simple class to represent a half-open interval.
|
||||
'''
|
||||
|
||||
start: int
|
||||
end: int
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'[{self.start}, {self.end})'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'>={self.start}, <{self.end}'
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return (self.start, self.end) < (other.start, other.end)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return (self.start, self.end) == (other.start, other.end)
|
||||
|
||||
def __contains__(self, item) -> bool:
|
||||
return item >= self.start and item < self.end
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.start < self.end
|
||||
|
||||
def size(self) -> int:
|
||||
return self.end - self.start
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open_output_file(path):
|
||||
'''
|
||||
Create a temporary file in the same directory as the specified path and
|
||||
replace it if the function succeeds. On non-Windows, the file replacement
|
||||
is atomic. On Windows, it is not.
|
||||
'''
|
||||
|
||||
directory = os.path.dirname(path)
|
||||
|
||||
with tempfile.NamedTemporaryFile(dir=directory, delete=False) as f:
|
||||
try:
|
||||
yield f
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows does not allow renaming a file with handles open
|
||||
f.close()
|
||||
|
||||
# Windows only supports atomic renames by calling
|
||||
# SetFileInformationByHandle() with the FileRenameInfoEx
|
||||
# operation and the FILE_RENAME_FLAG_REPLACE_IF_EXISTS and
|
||||
# FILE_RENAME_FLAG_POSIX_SEMANTICS flags. This is not exposed
|
||||
# in Python and it's not worth adding a new dependency for
|
||||
# doing low-level win32 API calls.
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
# NamedTemporaryFile always uses 600 permissions with no way to
|
||||
# override it. We'll do our own umask-respecting chmod.
|
||||
os.fchmod(f.fileno(), 0o666 & ~umask)
|
||||
|
||||
os.rename(f.name, path)
|
||||
except BaseException:
|
||||
if os.name == 'nt':
|
||||
# Windows does not allow deleting a file with handles open
|
||||
f.close()
|
||||
|
||||
os.unlink(f.name)
|
||||
raise
|
||||
|
||||
|
||||
def hash_file(f, hasher, buf_size=16384):
|
||||
'''
|
||||
Update <hasher> when the data from <f> until EOF.
|
||||
'''
|
||||
|
||||
buf = bytearray(buf_size)
|
||||
buf_view = memoryview(buf)
|
||||
|
||||
while True:
|
||||
n = f.readinto(buf_view)
|
||||
if not n:
|
||||
break
|
||||
|
||||
hasher.update(buf_view[:n])
|
||||
|
||||
return hasher
|
||||
|
||||
|
||||
def copyfileobj_n(f_in, f_out, size, buf_size=16384, hasher=None):
|
||||
'''
|
||||
Copy <size> bytes from <f_in> to <f_out>.
|
||||
|
||||
Raises IOError if EOF is reached in <f_in> before <size> bytes are read.
|
||||
'''
|
||||
|
||||
buf = bytearray(buf_size)
|
||||
buf_view = memoryview(buf)
|
||||
|
||||
while size:
|
||||
to_read = min(len(buf_view), size)
|
||||
n = f_in.readinto(buf_view[:to_read])
|
||||
if not n:
|
||||
break
|
||||
|
||||
if hasher:
|
||||
hasher.update(buf_view[:n])
|
||||
|
||||
f_out.write(buf_view[:n])
|
||||
size -= n
|
||||
|
||||
if size:
|
||||
raise IOError(f'Unexpected EOF; expected {size} more bytes')
|
||||
|
||||
|
||||
def decompress_n(decompressor, f_in, f_out, size, buf_size=16384, hasher=None):
|
||||
'''
|
||||
Read <size> bytes from <f_in> and decompress them to <f_out>.
|
||||
|
||||
Raises IOError if EOF is reached in <f_in> before <size> bytes are read.
|
||||
'''
|
||||
|
||||
buf = bytearray(buf_size)
|
||||
buf_view = memoryview(buf)
|
||||
|
||||
while size:
|
||||
to_read = min(len(buf_view), size)
|
||||
n = f_in.readinto(buf_view[:to_read])
|
||||
if not n:
|
||||
break
|
||||
|
||||
if hasher:
|
||||
hasher.update(buf_view[:n])
|
||||
|
||||
data = decompressor.decompress(buf_view[:n])
|
||||
|
||||
f_out.write(data)
|
||||
size -= n
|
||||
|
||||
if size:
|
||||
raise IOError(f'Unexpected EOF; expected {size} more bytes')
|
||||
elif not decompressor.eof:
|
||||
raise IOError('Did not reach end of compressed input')
|
||||
|
||||
|
||||
def zero_n(f_out, size, buf_size=16384):
|
||||
'''
|
||||
Write <size> zeroes to <f_out>.
|
||||
'''
|
||||
|
||||
buf = bytearray(buf_size)
|
||||
buf_view = memoryview(buf)
|
||||
|
||||
while size:
|
||||
to_write = min(len(buf_view), size)
|
||||
f_out.write(buf_view[:to_write])
|
||||
size -= to_write
|
||||
|
||||
|
||||
def read_exact(f, size: int) -> bytes:
|
||||
'''
|
||||
Read exactly <size> bytes from <f> or raise an EOFError.
|
||||
'''
|
||||
|
||||
data = f.read(size)
|
||||
if len(data) != size:
|
||||
raise EOFError(f'Unexpected EOF: expected {size} bytes, '
|
||||
f'but only read {len(data)} bytes')
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
# io.BytesIO returns a bytearray
|
||||
return bytes(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
def is_zero(data):
|
||||
'''
|
||||
Check if all bytes in the bytes-like object are null bytes.
|
||||
'''
|
||||
|
||||
view = memoryview(data)
|
||||
|
||||
while view:
|
||||
n = min(len(view), len(_ZERO_BLOCK))
|
||||
|
||||
if view[:n] != _ZERO_BLOCK[:n]:
|
||||
return False
|
||||
|
||||
view = view[n:]
|
||||
|
||||
return True
|
||||
@@ -1,195 +0,0 @@
|
||||
import contextlib
|
||||
import os
|
||||
import typing
|
||||
import unittest.mock
|
||||
|
||||
import avbtool
|
||||
|
||||
from . import openssl
|
||||
from . import util
|
||||
|
||||
|
||||
class SmuggledViaKernelCmdlineDescriptor:
|
||||
def __init__(self):
|
||||
self.kernel_cmdline = None
|
||||
|
||||
def encode(self):
|
||||
return self.kernel_cmdline.encode()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def smuggle_descriptors():
|
||||
'''
|
||||
Smuggle predefined vbmeta descriptors into Avb.make_vbmeta_image via the
|
||||
kernel_cmdlines parameter. The make_vbmeta_image function will:
|
||||
|
||||
* loop through kernel_cmdlines
|
||||
* create a AvbKernelCmdlineDescriptor instance for each item
|
||||
* assign kernel_cmdline to each descriptor instance
|
||||
* call encode on each descriptor
|
||||
'''
|
||||
|
||||
with unittest.mock.patch('avbtool.AvbKernelCmdlineDescriptor',
|
||||
SmuggledViaKernelCmdlineDescriptor):
|
||||
yield
|
||||
|
||||
|
||||
def _get_descriptor_overrides(
|
||||
avb: avbtool.Avb,
|
||||
images: dict[str, os.PathLike[str]],
|
||||
) -> typing.Tuple[dict[str, bytes], dict[str, avbtool.AvbDescriptor]]:
|
||||
'''
|
||||
Build a set of public key (chain) and hash/hashtree descriptor overrides
|
||||
that should be inserted in the parent vbmeta image for the given partition
|
||||
images.
|
||||
|
||||
If a partition image itself is signed, then a chain descriptor will be used.
|
||||
Otherwise, the existing hash or hashtree descriptor is used.
|
||||
'''
|
||||
|
||||
# Partition name -> raw public key
|
||||
out_public_keys = {}
|
||||
# Partition name -> descriptor
|
||||
out_descriptors = {}
|
||||
|
||||
# Construct descriptor overrides
|
||||
for name, path in images.items():
|
||||
image = avbtool.ImageHandler(path, read_only=True)
|
||||
footer, header, descriptors, image_size = avb._parse_image(image)
|
||||
|
||||
if name in out_public_keys or name in out_descriptors:
|
||||
raise ValueError(f'Duplicate partition name: {name}')
|
||||
|
||||
if header.public_key_size:
|
||||
# vbmeta is signed; use a chain descriptor
|
||||
blob = avb._load_vbmeta_blob(image)
|
||||
offset = header.SIZE + \
|
||||
header.authentication_data_block_size + \
|
||||
header.public_key_offset
|
||||
out_public_keys[name] = \
|
||||
blob[offset:offset + header.public_key_size]
|
||||
else:
|
||||
# vbmeta is unsigned; use the existing descriptor in the footer
|
||||
partition_descriptor = next(
|
||||
(d for d in descriptors
|
||||
if (isinstance(d, avbtool.AvbHashDescriptor)
|
||||
or isinstance(d, avbtool.AvbHashtreeDescriptor))
|
||||
and d.partition_name == name),
|
||||
None,
|
||||
)
|
||||
if partition_descriptor is None:
|
||||
raise ValueError(f'{path} has no descriptor for itself')
|
||||
|
||||
out_descriptors[name] = partition_descriptor
|
||||
|
||||
return (out_public_keys, out_descriptors)
|
||||
|
||||
|
||||
def get_vbmeta_deps(
|
||||
avb: avbtool.Avb,
|
||||
vbmeta_images: dict[str, os.PathLike[str]],
|
||||
) -> dict[str, set[str]]:
|
||||
'''
|
||||
Return the forward and reverse dependency tree for the specified vbmeta
|
||||
images.
|
||||
'''
|
||||
|
||||
deps = {}
|
||||
|
||||
for name, path in vbmeta_images.items():
|
||||
image = avbtool.ImageHandler(path, read_only=True)
|
||||
_, _, descriptors, _ = avb._parse_image(image)
|
||||
|
||||
deps.setdefault(name, set())
|
||||
|
||||
for d in descriptors:
|
||||
if isinstance(d, avbtool.AvbChainPartitionDescriptor) \
|
||||
or isinstance(d, avbtool.AvbHashDescriptor) \
|
||||
or isinstance(d, avbtool.AvbHashtreeDescriptor):
|
||||
deps[name].add(d.partition_name)
|
||||
deps.setdefault(d.partition_name, set())
|
||||
|
||||
return deps
|
||||
|
||||
|
||||
def patch_vbmeta_image(
|
||||
avb: avbtool.Avb,
|
||||
images: dict[str, os.PathLike[str]],
|
||||
input_path: os.PathLike[str],
|
||||
output_path: os.PathLike[str],
|
||||
key: os.PathLike[str],
|
||||
passphrase: str,
|
||||
padding_size: int,
|
||||
clear_flags: bool,
|
||||
):
|
||||
'''
|
||||
Patch the vbmeta image to reference the provided images.
|
||||
'''
|
||||
|
||||
# Load the original root vbmeta image
|
||||
image = avbtool.ImageHandler(input_path, read_only=True)
|
||||
footer, header, descriptors, image_size = avb._parse_image(image)
|
||||
|
||||
if header.flags != 0:
|
||||
if clear_flags:
|
||||
header.flags = 0
|
||||
else:
|
||||
raise ValueError(f'vbmeta flags disable AVB: 0x{header.flags:x}')
|
||||
|
||||
# Build a set of new descriptors in the same order as the original
|
||||
# descriptors, except with the descriptors patched to reference the given
|
||||
# images
|
||||
override_public_keys, override_descriptors = \
|
||||
_get_descriptor_overrides(avb, images)
|
||||
new_descriptors = []
|
||||
|
||||
for d in descriptors:
|
||||
if isinstance(d, avbtool.AvbChainPartitionDescriptor) and \
|
||||
d.partition_name in override_public_keys:
|
||||
d.public_key = override_public_keys.pop(d.partition_name)
|
||||
elif (isinstance(d, avbtool.AvbHashDescriptor) or \
|
||||
isinstance(d, avbtool.AvbHashtreeDescriptor)) and \
|
||||
d.partition_name in override_descriptors:
|
||||
d = override_descriptors.pop(d.partition_name)
|
||||
|
||||
new_descriptors.append(d)
|
||||
|
||||
if override_public_keys:
|
||||
raise Exception(f'Unused public key overrides: {override_public_keys}')
|
||||
if override_descriptors:
|
||||
raise Exception(f'Unused descriptor overrides: {override_descriptors}')
|
||||
|
||||
algorithm_name = avbtool.lookup_algorithm_by_type(header.algorithm_type)[0]
|
||||
|
||||
# Some older Pixel devices' vbmeta images are originally signed by a
|
||||
# 2048-bit RSA key, but avbroot expects RSA 4096 keys
|
||||
if algorithm_name == 'SHA256_RSA2048':
|
||||
algorithm_name = 'SHA256_RSA4096'
|
||||
|
||||
with util.open_output_file(output_path) as f:
|
||||
# Smuggle in the prebuilt descriptors via kernel_cmdlines
|
||||
with (
|
||||
smuggle_descriptors(),
|
||||
openssl.inject_passphrase(passphrase),
|
||||
):
|
||||
avb.make_vbmeta_image(
|
||||
output=f,
|
||||
chain_partitions=None,
|
||||
algorithm_name=algorithm_name,
|
||||
key_path=key,
|
||||
public_key_metadata_path=None,
|
||||
rollback_index=header.rollback_index,
|
||||
flags=header.flags,
|
||||
rollback_index_location=header.rollback_index_location,
|
||||
props=None,
|
||||
props_from_file=None,
|
||||
kernel_cmdlines=new_descriptors,
|
||||
setup_rootfs_from_kernel=None,
|
||||
include_descriptors_from_image=None,
|
||||
signing_helper=None,
|
||||
signing_helper_with_files=None,
|
||||
release_string=header.release_string,
|
||||
append_to_release_string=False,
|
||||
print_required_libavb_version=False,
|
||||
padding_size=padding_size,
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
[advisories]
|
||||
version = 2
|
||||
yanked = "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]
|
||||
version = 2
|
||||
include-dev = true
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[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"] },
|
||||
# Test files for liblzma's test suite
|
||||
{ name = "liblzma-sys", allow-globs = ["xz/tests/compress_prepared_bcj_*"] },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/bzip2-rs",
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "e2e"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.0"
|
||||
rsa = "0.9.6"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = { version = "0.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"]
|
||||
@@ -0,0 +1,26 @@
|
||||
# End-to-end tests
|
||||
|
||||
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 profile listed in the config, the test process will:
|
||||
|
||||
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
|
||||
|
||||
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 profiles listed in [`e2e.toml`](./e2e.toml), run:
|
||||
|
||||
```bash
|
||||
# To test all profiles
|
||||
cargo run --release -- test -a
|
||||
# Or to test against specific profiles
|
||||
cargo run --release -- test -p pixel_v4_gki -p pixel_v4_non_gki
|
||||
```
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# 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)
|
||||
|
||||
[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]
|
||||
original = "24a0a62cc08b96563f4872aee2fdd4a84d1a977b55c326dd9d4ddd92a1d326ea"
|
||||
patched = "29889670efea78bace221742b19e8ace88b67137fc2f46dcd9dbdf67e4e42267"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
|
||||
[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]
|
||||
original = "0e7d0924a68d46e00abe96abfa0e5f3a98d5d15a32bef7401b91fca9a19748e8"
|
||||
patched = "1a2f53d9ac3a1da75e5e7502110bda437c5a725764f16656c564d42460136279"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
|
||||
[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]
|
||||
original = "533e6f233cb98c98c945044c2ee81a6069e66baee6f7dbcfbf7523795a11215e"
|
||||
patched = "1eeae9dba0302c2d469bd03c8bccdc0469c171204dbd676a20cb6620d2d11c5c"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
|
||||
[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]
|
||||
original = "958dfa428abd2901178b90147903d7857ed78d6c016f6cb3af30d024a22a8f9a"
|
||||
patched = "b0d18ef350ca7b6499b7de4b0182f4ac3be7288ad238ce888708643e750f7631"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
XltUCz36vqCNSzspPZxFMGXah3kLyrTXDwfmasgn6nL4CtZDw5OeeLwlmkDuV2Im
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
7DsqL2Sk9T609OFpeVXwnrWHRrK3iazccxEDHWDqr5zJ9tgZkONhDvXhXuCQY76o
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use avbroot::cli::args::{LogFormat, LogLevel};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ProfileGroup {
|
||||
/// OTA profile name.
|
||||
#[arg(short, long, value_name = "NAME")]
|
||||
pub profile: Vec<String>,
|
||||
|
||||
/// All profiles.
|
||||
#[arg(short, long, conflicts_with = "profile")]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ConfigGroup {
|
||||
/// Path to config file.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "e2e.toml"
|
||||
)]
|
||||
pub config: PathBuf,
|
||||
|
||||
/// Working directory.
|
||||
///
|
||||
/// 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 profile: ProfileGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
}
|
||||
|
||||
/// List profiles in config file.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ListCli {
|
||||
#[command(flatten)]
|
||||
pub config: ConfigGroup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
Test(TestCli),
|
||||
List(ListCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Lowest log message severity to output.
|
||||
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
|
||||
pub log_level: LogLevel,
|
||||
|
||||
/// Output format for log messages.
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Sha256Hash(
|
||||
#[serde(
|
||||
serialize_with = "hex::serialize",
|
||||
deserialize_with = "hex::deserialize"
|
||||
)]
|
||||
pub [u8; 32],
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[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)]
|
||||
#[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)]
|
||||
#[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)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DmVerityData {
|
||||
pub content: DmVerityContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VbmetaData {
|
||||
pub deps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Data {
|
||||
Boot(BootData),
|
||||
DmVerity(DmVerityData),
|
||||
Vbmeta(VbmetaData),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Hashes {
|
||||
pub original: Sha256Hash,
|
||||
pub patched: Sha256Hash,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Partition {
|
||||
pub avb: Avb,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Profile {
|
||||
pub partitions: BTreeMap<String, Partition>,
|
||||
pub hashes: Hashes,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub ota_info: OtaInfo,
|
||||
#[serde(default)]
|
||||
pub profile: BTreeMap<String, Profile>,
|
||||
}
|
||||
|
||||
pub fn load_config(path: &Path) -> Result<(Config, DocumentMut)> {
|
||||
let contents =
|
||||
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
|
||||
let config: Config = toml_edit::de::from_str(&contents)
|
||||
.with_context(|| format!("Failed to parse config: {path:?}"))?;
|
||||
let document: DocumentMut = contents.parse().unwrap();
|
||||
|
||||
Ok((config, document))
|
||||
}
|
||||
+1198
File diff suppressed because it is too large
Load Diff
Vendored
-1
Submodule external/avb deleted from 3210440973
Vendored
-1
Submodule external/build deleted from 2014bbb8e7
Vendored
-1
Submodule external/update_engine deleted from e47767a5a8
@@ -1,62 +0,0 @@
|
||||
# avbroot extra
|
||||
|
||||
This directory contains some extra scripts that aren't required for avbroot's operation, but may be useful for troubleshooting.
|
||||
|
||||
## `bootimagetool`
|
||||
|
||||
This is a frontend to the [`avbroot/formats/bootimage.py`](../avbroot/formats/bootimage.py) library for working with boot images.
|
||||
|
||||
### Unpacking a boot image
|
||||
|
||||
```bash
|
||||
python bootimagetool.py unpack <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.json` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
|
||||
|
||||
### Packing a boot image
|
||||
|
||||
```bash
|
||||
python bootimagetool.py pack <output boot image>
|
||||
```
|
||||
|
||||
This subcommand packs a new boot image from the individual components in the current directory by default (see `--help`). The default input filenames are the same as the output filenames for the `unpack` subcommand.
|
||||
|
||||
### Repacking a boot image
|
||||
|
||||
```bash
|
||||
python bootimagetool.py repack <input boot image> <output boot image>
|
||||
```
|
||||
|
||||
This subcommand repacks a boot image without writing the individual components to disk first. This is useful for roundtrip testing of avbroot's boot image parser. The output should be identical to the input, minus any footers, like the AVB footer. The only exception is the VTS signature for v4 boot images, which is always stripped out.
|
||||
|
||||
## `cpiotool`
|
||||
|
||||
This is a frontend to the [`avbroot/formats/compression.py`](../avbroot/formats/compression.py) and [`avbroot/formats/cpio.py`](../avbroot/formats/cpio.py) libraries. It is useful for inspecting compressed and uncompressed cpio archives.
|
||||
|
||||
### Dumping a cpio archive
|
||||
|
||||
```bash
|
||||
python cpiotool.py dump <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 base64-encoded. The base64-encoded data is truncated to 5 lines by default to avoid outputting too much data, but this behavior can be disabled with `--no-truncate`.
|
||||
|
||||
### Repacking a cpio archive
|
||||
|
||||
```bash
|
||||
python cpiotool.py repack <input cpio archive> <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:
|
||||
|
||||
* files are sorted by name
|
||||
* inodes are reassigned, starting from 300000
|
||||
* there is no excess padding at the end of the file
|
||||
|
||||
The compressed output may differ from what other tools produce because:
|
||||
|
||||
* LZ4 legacy chunks are packed to exactly 8 MiB, except for the last chunk, which may be smaller.
|
||||
* LZ4 legacy uses the high compression mode with a compression level of 12.
|
||||
* The GZIP header has the modification timestamp set to 0 (Unix epoch time).
|
||||
* GZIP uses a compression level of 9.
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(sys.path[0], '..'))
|
||||
from avbroot.formats import bootimage
|
||||
|
||||
|
||||
class BytesDecoder(json.JSONDecoder):
|
||||
def __init__(self):
|
||||
super().__init__(object_hook=self.from_dict)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d):
|
||||
# This is insufficient for arbitrary data, but we're not dealing with
|
||||
# arbitrary data
|
||||
if 'type' in d:
|
||||
if d['type'] == 'UTF-8':
|
||||
return d['data'].encode('UTF-8')
|
||||
elif d['type'] == 'hex':
|
||||
return bytes.fromhex(d['data'])
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class BytesEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, bytes):
|
||||
if b'\0' not in obj:
|
||||
try:
|
||||
return {
|
||||
'type': 'UTF-8',
|
||||
'data': obj.decode('UTF-8'),
|
||||
}
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
return {
|
||||
'type': 'hex',
|
||||
'data': obj.hex(),
|
||||
}
|
||||
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
def read_or_none(path):
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def write_if_not_none(path, data):
|
||||
if data is not None:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser_kwargs = {'formatter_class': argparse.ArgumentDefaultsHelpFormatter}
|
||||
|
||||
parser = argparse.ArgumentParser(**parser_kwargs)
|
||||
subparsers = parser.add_subparsers(dest='subcommand', required=True,
|
||||
help='Subcommands')
|
||||
|
||||
base = argparse.ArgumentParser(add_help=False)
|
||||
base.add_argument('-q', '--quiet', action='store_true',
|
||||
help='Do not print header information')
|
||||
|
||||
pack = subparsers.add_parser('pack', help='Pack a boot image',
|
||||
parents=[base], **parser_kwargs)
|
||||
unpack = subparsers.add_parser('unpack', help='Unpack a boot image',
|
||||
parents=[base], **parser_kwargs)
|
||||
repack = subparsers.add_parser('repack', help='Repack a boot image',
|
||||
parents=[base], **parser_kwargs)
|
||||
|
||||
for p in (pack, unpack):
|
||||
prefix = '--input-' if p == pack else '--output-'
|
||||
|
||||
p.add_argument('boot_image', help='Path to boot image')
|
||||
|
||||
p.add_argument(prefix + 'header', default='header.json',
|
||||
help='Path to header JSON')
|
||||
p.add_argument(prefix + 'kernel', default='kernel.img',
|
||||
help='Path to kernel')
|
||||
p.add_argument(prefix + 'ramdisk-prefix', default='ramdisk.img.',
|
||||
help='Path prefix for ramdisk')
|
||||
p.add_argument(prefix + 'second', default='second.img',
|
||||
help='Path to second stage bootloader')
|
||||
p.add_argument(prefix + 'recovery-dtbo', default='recovery_dtbo.img',
|
||||
help='Path to recovery dtbo/acpio')
|
||||
p.add_argument(prefix + 'dtb', default='dtb.img',
|
||||
help='Path to device tree blob')
|
||||
p.add_argument(prefix + 'bootconfig', default='bootconfig.txt',
|
||||
help='Path to bootconfig')
|
||||
|
||||
repack.add_argument('input', help='Path to input boot image')
|
||||
repack.add_argument('output', help='Path to output boot image')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
if args.subcommand == 'pack':
|
||||
with open(args.input_header, 'r') as f:
|
||||
data = json.load(f, cls=BytesDecoder)
|
||||
|
||||
img = bootimage.create_from_dict(data)
|
||||
|
||||
img.kernel = read_or_none(args.input_kernel)
|
||||
img.second = read_or_none(args.input_second)
|
||||
img.recovery_dtbo = read_or_none(args.input_recovery_dtbo)
|
||||
img.dtb = read_or_none(args.input_dtb)
|
||||
img.bootconfig = read_or_none(args.input_bootconfig)
|
||||
|
||||
for i in itertools.count():
|
||||
ramdisk = read_or_none(f'{args.input_ramdisk_prefix}{i}')
|
||||
if ramdisk is None:
|
||||
break
|
||||
|
||||
img.ramdisks.append(ramdisk)
|
||||
|
||||
if not args.quiet:
|
||||
print(img)
|
||||
|
||||
with open(args.boot_image, 'wb') as f:
|
||||
img.generate(f)
|
||||
|
||||
elif args.subcommand == 'unpack':
|
||||
with open(args.boot_image, 'rb') as f:
|
||||
img = bootimage.load_autodetect(f)
|
||||
if not args.quiet:
|
||||
print(img)
|
||||
|
||||
with open(args.output_header, 'w') as f:
|
||||
json.dump(img.to_dict(), f, indent=4, cls=BytesEncoder)
|
||||
|
||||
write_if_not_none(args.output_kernel, img.kernel)
|
||||
write_if_not_none(args.output_second, img.second)
|
||||
write_if_not_none(args.output_recovery_dtbo, img.recovery_dtbo)
|
||||
write_if_not_none(args.output_dtb, img.dtb)
|
||||
write_if_not_none(args.output_bootconfig, img.bootconfig)
|
||||
|
||||
for i, ramdisk in enumerate(img.ramdisks):
|
||||
write_if_not_none(f'{args.output_ramdisk_prefix}{i}', ramdisk)
|
||||
|
||||
elif args.subcommand == 'repack':
|
||||
with open(args.input, 'rb') as f:
|
||||
img = bootimage.load_autodetect(f)
|
||||
if not args.quiet:
|
||||
print(img)
|
||||
|
||||
with open(args.output, 'wb') as f:
|
||||
img.generate(f)
|
||||
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(sys.path[0], '..'))
|
||||
from avbroot.formats import compression
|
||||
from avbroot.formats import cpio
|
||||
|
||||
|
||||
CONTENT_BEGIN = '----- BEGIN UTF-8 CONTENT -----'
|
||||
CONTENT_END = '----- END UTF-8 CONTENT -----'
|
||||
CONTENT_END_NO_NEWLINE = '----- END UTF-8 CONTENT (NO NEWLINE) -----'
|
||||
|
||||
BASE64_BEGIN = '----- BEGIN BASE64 CONTENT -----'
|
||||
BASE64_END = '----- END BASE64 CONTENT -----'
|
||||
BASE64_END_TRUNCATED = '----- END BASE64 CONTENT (TRUNCATED) -----'
|
||||
|
||||
NO_DATA = '----- NO DATA -----'
|
||||
|
||||
|
||||
def print_content(data, truncate=False):
|
||||
if not data:
|
||||
print(NO_DATA)
|
||||
return
|
||||
|
||||
if b'\0' not in data:
|
||||
try:
|
||||
data_str = data.decode('UTF-8')
|
||||
|
||||
if CONTENT_BEGIN not in data_str \
|
||||
and CONTENT_END not in data_str \
|
||||
and CONTENT_END_NO_NEWLINE not in data_str:
|
||||
print(CONTENT_BEGIN)
|
||||
print(data_str, end='')
|
||||
if data_str[-1] != '\n':
|
||||
print()
|
||||
print(CONTENT_END_NO_NEWLINE)
|
||||
else:
|
||||
print(CONTENT_END)
|
||||
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
data_base64 = base64.b64encode(data).decode('ascii')
|
||||
|
||||
print(BASE64_BEGIN)
|
||||
for i, offset in enumerate(range(0, len(data_base64), 76)):
|
||||
if truncate and i == 5:
|
||||
print(BASE64_END_TRUNCATED)
|
||||
return
|
||||
|
||||
print(data_base64[offset:offset + 76])
|
||||
print(BASE64_END)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='subcommand', required=True,
|
||||
help='Subcommands')
|
||||
|
||||
dump = subparsers.add_parser('dump', help='Dump cpio headers and data')
|
||||
repack = subparsers.add_parser('repack', help='Repack cpio archive')
|
||||
|
||||
dump.add_argument('--no-truncate', action='store_true',
|
||||
help='Do not truncate binary file contents')
|
||||
|
||||
for p in (dump, repack):
|
||||
p.add_argument('input', help='Path to input cpio file')
|
||||
|
||||
repack.add_argument('output', help='Path to output cpio file')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_archive(path, **cpio_kwargs):
|
||||
with open(path, 'rb') as f_raw:
|
||||
with compression.CompressedFile(f_raw, 'rb', raw_if_unknown=True) as f:
|
||||
return cpio.load(f.fp, **cpio_kwargs), f.format
|
||||
|
||||
|
||||
def save_archive(path, entries, format):
|
||||
with open(path, 'wb') as f_raw:
|
||||
with compression.CompressedFile(f_raw, 'wb', format=format,
|
||||
raw_if_unknown=True) as f:
|
||||
cpio.save(f.fp, entries)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
if args.subcommand == 'dump':
|
||||
entries, format = load_archive(
|
||||
args.input,
|
||||
# We want to show the headers exactly as they are
|
||||
include_trailer=True,
|
||||
reassign_inodes=False,
|
||||
)
|
||||
|
||||
print('Compression format:', format)
|
||||
print()
|
||||
|
||||
for entry in entries:
|
||||
print(entry)
|
||||
print_content(entry.content, truncate=not args.no_truncate)
|
||||
print()
|
||||
|
||||
elif args.subcommand == 'repack':
|
||||
entries, format = load_archive(args.input)
|
||||
save_archive(args.output, entries, format)
|
||||
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user