Compare commits

..

544 Commits

Author SHA1 Message Date
Andrew Gunnerson 59cf37faaf Version 3.18.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:41:13 -04:00
Andrew Gunnerson 36269acd7b CHANGELOG.md: Add entry for PR #475
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:37:45 -04:00
Andrew Gunnerson 6aacc5a76c Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:33:54 -04:00
Andrew Gunnerson bd4ebde403 CHANGELOG.md: Add entry for PR #474
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:30:23 -04:00
Andrew Gunnerson ffdae0bf88 Bump Magisk version upper bound to 30300
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:28:33 -04:00
Andrew Gunnerson 8282f8087c CHANGELOG.md: Add entry for PR #473
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:27:46 -04:00
Andrew Gunnerson 6b11cd8af2 Allow using SHA-1 as AVB hash algorithm
Previously, we automatically promoted SHA-1 to SHA-256 because SHA-1 is
insecure, but there are devices that don't support SHA-256. It's safer
to just keep using the original hash algorithm.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:17:21 -04:00
Andrew Gunnerson f610f3b794 CHANGELOG.md: Add entry for PR #470
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:14:26 -04:00
Andrew Gunnerson b8d022d52c format/ota: Make property files verification more lenient
Previously, we computed the expected property files string (based on
AOSP's rules) and checked if the string in the OTA metadata was a
byte-for-byte match. This would fail for OTAs with strings that differ
from how AOSP generates them. This could be additional files or just
different ordering of the entries.

This commit changes the approach to just verify that the property file
entries is a valid subset of the zip file entries. We no longer try to
compute the expected value.

This does not change what avbroot generates when patching an OTA. Newly
generated property files strings always follow AOSP's rules.

Fixes: #469

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-22 20:49:05 -04:00
Andrew Gunnerson cd38217111 Version 3.17.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-03 14:48:16 -04:00
Andrew Gunnerson c8d548d224 CHANGELOG.md: Add entry for PR #468
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-03 14:31:59 -04:00
Andrew Gunnerson 9ad430ac7f Bump Magisk version upper bound to 30200
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-03 14:30:50 -04:00
Andrew Gunnerson 8ca4eb5ad9 Version 3.17.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 19:07:13 -04:00
Andrew Gunnerson e2b1ccb7a1 CHANGELOG.md: Add entry for PR #467
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 19:06:37 -04:00
Andrew Gunnerson 71a31ae01b Bump Magisk version upper bound to 30100
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 18:55:14 -04:00
Andrew Gunnerson 83d7ffbc5e CHANGELOG.md: Add entry for PR #464
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 20:20:54 -04:00
Andrew Gunnerson e397998d9e Update dependencies
The zip crate gained support for streaming writes in its master branch,
so we can finally upgrade from our ancient fork of it. The new
implementation is done a bit differently, with seekable writers having
the ZipWriter<W> type and streaming writers having the
ZipWriter<StreamWriter<W>> type. This forces us to add a new wrapper
type since we have to switch between them at runtime.

We still need to maintain a (hopefully temporary) fork of the crate due
to a few issues:

1. There's no way to get the original underlying writer instance back
   after finalizing a streaming zip. A fix for this has been submitted
   upstream:
   https://github.com/zip-rs/zip2/pull/367

2. The streaming writes implementation does not include the magic
   signature for data descriptors. While the zip spec says the magic
   value is optional and parsers should not require it, older versions
   of Android's libziparchive do. A fix for this has been submitted
   upstream:
   https://github.com/zip-rs/zip2/pull/368

3. There is currently no way to get the data offset of zip entries.
   avbroot requires this to fill in the OTA metadata's "property files"
   entries, which Android uses to read file data without parsing the zip
   file structures.

This new zip update produces files that are slightly different to
before. The "version made by" and "version needed to extract" fields are
now set to their minimum possible values. Previously, the zip crate was
hardcoded to use versions 4.6 and 2.0, respectively.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 18:55:08 -04:00
Andrew Gunnerson 8ef22508f5 CHANGELOG.md: Add entry for PR #463
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 18:49:32 -04:00
Andrew Gunnerson bf42a6a75c e2e: Split streaming and seekable work directories
Makes troubleshooting easier when files aren't being overwritten.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 17:49:00 -04:00
lefuglyduck a2fe6fc9d8 Update README.md
Clarify the updates section so it doesn't "loop back" to the usage section. 

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-12 16:12:13 -07:00
lefuglyduck faeb1fe988 Update README.md
Minor grammatical change.

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-12 17:53:52 -04:00
Andrew Gunnerson f1b2c6f468 Merge PR #458
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-12 17:51:40 -04:00
lefuglyduck 2f964bf113 Update README.md
Clarify step 3 of usage section.

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-11 22:36:24 -07:00
Andrew Gunnerson f393d7adc4 README.md: Add warning about post-installation snapshot merge operation
Issue: #454

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-08 17:00:21 -04:00
Andrew Gunnerson 72a1c3f216 Version 3.17.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 12:00:31 -04:00
Andrew Gunnerson 2db8d3826e CHANGELOG.md: Add entry for PR #453
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:26:46 -04:00
Andrew Gunnerson 1448e55205 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:21:14 -04:00
Andrew Gunnerson b3862a9c4a CHANGELOG.md: Add entry for PR #452
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:18:11 -04:00
Andrew Gunnerson 088db04673 ota: Reserve 16 bytes for OTA metadata property files
Our previous limit was 15 bytes for the <offset>:<size> placeholder,
matching AOSP's ota_utils.py. Since the size of metadata.pb is almost
always 4 digits, this leaves 10 digits for the offset, which isn't
enough for large OTAs. AOSP never actually hits the limit because it
puts metadata and metadata.pb at the beginning of the output zip file.
We put the files at the end of the zip since we do streaming writes.

Fixes: #451

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:16:45 -04:00
Andrew Gunnerson b1410c869d Version 3.16.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:54:52 -04:00
Andrew Gunnerson dd6eaf8e78 CHANGELOG.md: Add entry for PR #449
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:50:59 -04:00
Andrew Gunnerson 0eac8e6614 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:37:32 -04:00
Andrew Gunnerson 8a4f90176e CHANGELOG.md: Add entry for PR #448
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:33:01 -04:00
Andrew Gunnerson bd166594e2 Bump Magisk version upper bound to 29100
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:31:34 -04:00
Andrew Gunnerson 19129ae927 Version 3.16.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 18:15:02 -04:00
Andrew Gunnerson fb2aaed042 CHANGELOG.md: Add entry for PR #446
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 18:14:25 -04:00
Andrew Gunnerson 182d937d34 Update all dependencies
This also fixes a number of disabled-by-default clippy warnings and
updates the Rust edition to 2024.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 18:06:59 -04:00
Andrew Gunnerson 113bdec6dc CHANGELOG.md: Add entry for PR #445
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 16:57:03 -04:00
Andrew Gunnerson 1f0d012ac0 e2e: Fix incorrect metadata when generating a non-CoW-V2 payload
It was hardcoded to set the CoW version to v2 in the payload manifest.
This commit also updates the pixel_v2 profile to disable VABC so that
scenario gets tested. The pixel_v3 profile now uses CoW v2 with gz.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 16:55:38 -04:00
Andrew Gunnerson e4fbe00ea2 CHANGELOG.md: Add entry for PR #444
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 16:29:40 -04:00
Andrew Gunnerson ad0b3d5aa8 payload: Add support for custom CoW compression levels
No known device uses this functionality, but AOSP supports it, so we
should too.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 23:34:25 -04:00
Andrew Gunnerson f4394b0c21 CHANGELOG.md: Add entry for PR #443
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 23:03:44 -04:00
Andrew Gunnerson 9dd98fd715 payload: Add support for uncompressed CoW and fix more estimation bugs
No known device uses this, but it's very useful for testing that our
overhead calculation is identical to AOSP's. A few more bugs were found
in our overhead calculation logic:

* The CowHeaderV3 size was missing the original CowHeader (v2) fields
  that are supposed to be included due to inheritance in the C++ class.
* The additional 1% overhead was incorrectly calculated against the
  initial CoW estimate before static overhead for CoW headers was added.
* The V3 num_ops estimation did not set a minimum of 25 to match
  delta_generator.
* The V2 size estimation did not take into account that a cluster of CoW
  operations cannot be truncated. It must be a multiple of cluster_ops
  (200 for avbroot).

With these fixes, the CoW estimation when compression is disabled
matches AOSP exactly. This means all the overhead calculation is now
correct and the only difference when compression is enabled is in the
compression ratios of the various lz4/gz implementations.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 22:59:15 -04:00
Andrew Gunnerson b6e3c68241 CHANGELOG.md: Add entry for PR #442
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 20:39:57 -04:00
Andrew Gunnerson 6fee5346bb payload: Add support for CoW version 3
AOSP has long supported CoW version 3, but it wasn't used by the stock
OS on any Pixel devices until the new Pixel 9a.

CoW version 3 is fundamentally similar to version 2, though with
differences in the main header and how the operation headers are stored.
The compression is no longer done in fixed-size chunks equal to the
block size. Instead, the payload specifies a "compression factor", which
is the maximum chunk size to pass to the compressor. The actual chunk
size is the largest power of 2 <= the compression factor and the
remaining input size. Additionally, for version 3, the payload stores an
additional estimate_op_count_max field containing the number of CoW
operations.

While working on support for version 3, a few bugs in the version 2
estimation logic were found and fixed:

* The cluster_ops * sizeof(CowOperationV2) overhead incorrectly
  assumed that cluster_ops was a constant 200 instead of the actual
  number of CoW operations.
* The overhead did not account for kCowLabelOp headers, which
  delta_generator emits once for every InstallOperation in the payload.
* The overhead did not account for kCowClusterOp headers, which batch
  CoW operations into groups of 200.
* The overhead did not account for the CowFooter.

The version 3 overhead is much simpler and easier to calculate compared
to version 2.

Fixes: #441

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 20:28:50 -04:00
Ivan 7d786150cf README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/de433a27245adb26e578bdeb34a404552c79ea85
* https://github.com/chenxiaolong/avbroot/commit/5d66774d131505928b31e1968da0498732ac28ab
* https://github.com/chenxiaolong/avbroot/commit/b922f0d23ce841ac0bd14bfd33da46273f183f68
2025-04-21 19:51:43 +03:00
Andrew Gunnerson b922f0d23c README.md: Deemphasize rooting in project description
While avbroot initially started as a way to allow a rooted boot image to
be used with a locked bootloader, it has evolved much since then.
Nowadays, many folks use it to make modifications to their OTAs that
don't involve enabling root access. avbroot also has many subcommands
for packing and unpacking various Android image formats that people use
without ever using avbroot's main OTA patching functionality.

This commit updates the project description to reflect this and
simplifies the wording a bit.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-18 20:51:54 -04:00
Andrew Gunnerson 1f2b2170b3 CHANGELOG.md: Fix incorrect link to VABC compression algorithm documentation
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-11 21:13:17 -04:00
Andrew Gunnerson da124e4e05 Version 3.15.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-06 00:09:21 -04:00
Andrew Gunnerson 7b778515d1 CHANGELOG.md: Add entry for PR #439
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-06 00:02:48 -04:00
Andrew Gunnerson 98745afe33 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:54:20 -04:00
Andrew Gunnerson a47c501211 CHANGELOG.md: Add entry for PR #438
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:53:50 -04:00
Andrew Gunnerson f2e47a65d4 Switch back to ring
This reverts commit e929ecbe44.

Ring is back to being maintained again, so let's switch back to it since
it has fewer build dependencies and is much faster to compile.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:52:53 -04:00
Andrew Gunnerson e11ddd2ba7 CHANGELOG.md: Add entry for PR #437
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:38:51 -04:00
Andrew Gunnerson 5d66774d13 Add support for changing VABC CoW compression algorithm
Devices that launch with Android <14 generally use gzip as the CoW
compression algorithm. This never changes because future full OTAs
always need to be installable from the version of Android the device
launched with.

However, for users that don't care about the upgrade path from old
versions of Android, a new --vabc-algo option can be used to switch from
gz to lz4 compression. This can cut down the OTA installation time by
more than 2/3rds when installing via a custom OTA updater app. On my
Pixel Tablet, the installation time for the update_engine DOWNLOADING
phase decreased from 32:05 to 9:41. Note that this has absolutely no
effect on the performance when sideloading from recovery mode because
that does not use CoW.

When this new option is used, all dynamic partitions need to be
extracted from the OTA during patching so that the CoW estimates can be
recomputed. This will slow down the patching process and use up more
temporary disk space.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:06:13 -04:00
Andrew Gunnerson 3f09a506a0 Version 3.14.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:27:05 -04:00
Andrew Gunnerson 4664f8ea37 CHANGELOG.md: Add entry for PR #435
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:26:29 -04:00
Andrew Gunnerson 796e2a4fa2 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:17:15 -04:00
Andrew Gunnerson e6b60d5d0f CHANGELOG.md: Add entry for PR #434
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:02:39 -04:00
Andrew Gunnerson e8cb4a8d53 Fix incorrect compression input for gzip CoW size estimation
Instead of compressing the 64 MiB input in 2 MiB chunks, each loop
iteration was compressing the full 64 MiB. This massively slowed down
the patching process from seconds to potentially hours and would
temporarily waste a bunch of space during OTA installation.

Fixes: #433

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 18:50:44 -04:00
Andrew Gunnerson b2d280eb20 CHANGELOG.md: Add entry for PR #430
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 20:23:48 -04:00
Andrew Gunnerson e2dc5174b4 format/ota: Decouple OTA signature parsing from verification
This way, we can fail hard for parsing errors, but not for verification
errors in `avbroot ota verify`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 20:22:16 -04:00
Andrew Gunnerson 80c47e9a02 CHANGELOG.md: Add entry for PR #429
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:49:54 -04:00
Andrew Gunnerson 65ba3ad5cc Fix clippy 1.85 warnings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:48:32 -04:00
Andrew Gunnerson a7438876ce CHANGELOG.md: Add entry for PR #428
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:38:16 -04:00
Andrew Gunnerson c039901a85 cli/ota: Improve ota verify otacerts.zip handling
* Try to avoid fail-fast behavior to show as many errors as possible.
  Parsing errors always fail immediately, but verification errors don't.
* Move the recovery otacerts.zip check to the end to let more important
  checks run first.
* Improve error message when otacerts.zip does not contain the signing
  certificate for the OTA to make it clear the issue is not that the zip
  contains no certificates at all.
* Always run the recovery otacerts.zip check, but just log the error as
  a warning when running with --skip-recovery-ota-cert.
* Fix unformatted error context string when parsing a boot image's
  otacerts.zip file fails.

Discussion: #426

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:34:42 -04:00
Andrew Gunnerson 0cdc7172dd Version 3.13.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:39:59 -04:00
Andrew Gunnerson 1296275418 CHANGELOG.md: Add entry for PR #427
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:38:43 -04:00
Andrew Gunnerson a62d0a5c91 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:25:35 -04:00
Andrew Gunnerson de433a2724 README.md: Clarify what --skip-{system,recovery}-ota-cert affects
Discussion: #426

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:07:17 -04:00
Andrew Gunnerson c08af33343 CHANGELOG.md: Add entry for PR #425
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-24 21:17:44 -05:00
Andrew Gunnerson e224884591 format/lp: Relax validation checks to parse on-device super partition
There are two documented behaviors in liblp that are violated with the
on-disk super partition layout after a virtual A/B CoW merge:

1. The partition name includes a `-` due to the `-cow` suffix. This is
   not meant to be a valid character.
2. The extent list is likely to have many gaps and not be sorted. The
   format documentation says that gaps are not allowed.

This commit updates avbroot's LP parser to be less strict so that it can
load real on-device super partitions. The extent allocator for the
`pack` subcommand remains unchanged though, so avbroot will always
produce LP images with sorted, gapless extents.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-24 21:05:28 -05:00
Andrew Gunnerson a525fc4550 CHANGELOG.md: Add entry for PR #424
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:34:30 -05:00
Andrew Gunnerson 8c6b0fdfc5 cli/ota: Discard unmodified system image temp file earlier
This is the same optimization as is currently done for boot images.
There's no reason to keep the temp file around for the entire patching
process if it's unmodified and we're not going to be copying its data
into the payload.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:23:10 -05:00
Andrew Gunnerson 36d4ed19ad CHANGELOG.md: Add entry for PR #423
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:18:12 -05:00
Andrew Gunnerson f9b06b33e6 cli/ota: Fix incorrect unprotected partition warning with --skip-system-ota-cert
The filtering out of partitions was done at the wrong scope, causing
avbroot to warn that extracted-but-unmodified partitions were not
protected by AVB. We never encountered this before because the system
image was always patched and unmodified boot images got filtered out at
an earlier phase during patching.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:00:59 -05:00
Andrew Gunnerson 620c873be5 CHANGELOG.md: Add entry for PR #422
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:00:20 -05:00
Andrew Gunnerson e929ecbe44 Replace ring with aws-lc
The author of ring recently announced that the library is no longer
being maintained and fixes for security issues may be significantly
delayed. Big thanks to Brian Smith for creating and maintaining the
library for so long!

This commit replaces ring with aws-lc, a cryptography library maintained
by Amazon AWS. It seems to be well-regarded and is used by high-profile
projects like rustls. It is also API-compatible with ring, so it is
effectively a drop-in replacement.

Unfortunately, we still cannot switch back to the RustCrypto SHA1 and
SHA2 implementations because they are still significantly slower than
ring and aws-lc on systems that do not support the SHA-NI extensions.

https://rustsec.org/advisories/RUSTSEC-2025-0007

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 20:44:19 -05:00
Andrew Gunnerson 31685713ef CHANGELOG.md: Add entry for PR #421
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 19:33:58 -05:00
Andrew Gunnerson 0dab7aa32c Switch to stable bzip2-rs release and use zlib-rs flate2 backend
* There is now a stable release of bzip2-rs with the fix for both the C
  and Rust versions of bzip2 being compiled.

* The zlib-rs deflate implementation is faster than the default
  miniz_oxide. Changing this requires updating the checksums in the e2e
  tests due to slight differences in compression levels between the two
  implementations.

* Temporarily silence RUSTSEC-2025-0007 to avoid blocking CI. The ring
  library is no longer maintained.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 19:27:18 -05:00
Ivan ba6c1b1400 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/c198646c7c9b3eb8013dca43df6df8502e619e3c
* https://github.com/chenxiaolong/avbroot/commit/d4eb231dd49e7e6c7320135785cde7cd57634a50
* https://github.com/chenxiaolong/avbroot/commit/1484cd47c354197e1179a554f9f001020297aac8
* https://github.com/chenxiaolong/avbroot/commit/84fa6c6bc63b62d1b3b96dcb50b18b13dea0076f

Signed-off-by: Ivan <reddxae@proton.me>
2025-02-11 14:20:11 +03:00
Andrew Gunnerson 1ecbf1144d CHANGELOG.md: Add entry for PR #418
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-09 21:59:13 -05:00
Andrew Gunnerson 84fa6c6bc6 Add option to skip replacing OTA cert in system image
This is analogous to the existing --skip-recovery-ota-cert option,
except for the system image.

Discussion: #417

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-09 21:42:18 -05:00
Andrew Gunnerson 15b7db4631 CHANGELOG.md: Add entry for PR #415
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-04 00:16:38 -05:00
Andrew Gunnerson 1d9c1574da lp: Stop checking for zeroed first block
AOSP says that for non-empty images, the first 4 KiB block is supposed
to be filled with zeros to prevent it from being interpreted as an old
BIOS boot sector. The previous implementation relied on that to
distinguish between empty and non-empty images. However, Samsung decided
to use this region for their own SignerVer02 structure, so the heuristic
doesn't work.

AOSP's liblp tries to parse the input file as an empty image before
falling back to parsing as a normal image. We'll do the same.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-04 00:13:27 -05:00
Andrew Gunnerson 43db728b9d Version 3.12.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:20:06 -05:00
Andrew Gunnerson fbe9f629ab CHANGELOG.md: Add entry for PR #411
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:11:40 -05:00
Andrew Gunnerson 80d5f19223 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:10:42 -05:00
Andrew Gunnerson cff5ac6b2e CHANGELOG.md: Add entry for PR #410
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:04:35 -05:00
Andrew Gunnerson 2e3b5db9fe cli/key: Rename extract-avb to encode-avb
For consistency with decode-avb. The old syntax will remain supported
indefinitely for backwards compatibility.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:01:24 -05:00
Andrew Gunnerson 58a279f3a4 CHANGELOG.md: Add entry for PR #409
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 17:53:08 -05:00
Andrew Gunnerson 1484cd47c3 cli/ota: Add support for extracting OTA certificate and AVB public key
This also adds a new --none option so that these two components can be
extracted without extracting any partition images.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 17:50:09 -05:00
Andrew Gunnerson 2ee38b716b CHANGELOG.md: Add entry for PR #408
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 16:48:08 -05:00
Andrew Gunnerson d4eb231dd4 cli/ota: Add support for extracting specific images
Previously, we only supported extracting all images or the subset of
images that could potentially be patched by avbroot. This was
unnecessarily slow if the user only needed to extract a specific image.

This commit also deprecates and hides the `--boot-only` option, though
the functionality will remain indefinitely.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 16:43:35 -05:00
Andrew Gunnerson cf064e145d Version 3.11.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 20:05:57 -05:00
Andrew Gunnerson 3b0c97a93d CHANGELOG.md: Add entry for PR #404
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 20:05:30 -05:00
Andrew Gunnerson 2d4f08f48b Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 19:56:02 -05:00
Andrew Gunnerson a753304dff CHANGELOG.md: Add entry for PR #403
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 19:39:42 -05:00
Andrew Gunnerson 9480e2ffa4 cpio: Read and write entire header at once
Avoids doing 14 small reads per cpio entry.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 19:37:58 -05:00
Andrew Gunnerson 140d0ddd8b CHANGELOG.md: Add entry for PR #402
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 15:08:11 -05:00
Andrew Gunnerson e0b114cf71 Switch to upstream master branch of bzip2-rs
The only remaining fix from our fork has been merged. This can be
switched to a stable release after the next release.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 14:59:44 -05:00
Andrew Gunnerson 78449f686d CHANGELOG.md: Add entry for PR #401
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 14:54:51 -05:00
Andrew Gunnerson a2fb807803 Remove all uses of implicit error propagation
Implicit error propagation was originally used because it was convenient
and made it easy to just bubble up errors via the ? operator without
thinking. However, there have been too many situations where this
resulted in error messages that were completely useless in
troubleshooting the problem. "I/O error", even with a specific reason
attached, is useless where there are potentially hundreds of operations
where I/O can fail.

I no longer think implicit error propagation is a good idea, so this
commit removes every single use of #[from] in every custom error type.
There are now many more error variants, allowing more context to be
attached to the underlying errors.

Previously, it was easy to encounter error messages like:

    Caused by:
        0: Failed to patch payload: payload.bin
        1: Failed to patch boot images: boot, init_boot, vendor_boot
        2: Boot image error
        3: I/O error
        4: failed to fill whole buffer

This is a terrible error message because it doesn't mention which of the
3 boot images failed to parse, nor does it mention during which I/O
operation it encountered EOF. With this commit, this sort of information
is now included. For example, if the boot image happened to be truncated
in the middle of the ramdisk, the error message would now be:

    Caused by:
        0: Failed to patch payload: payload.bin
        1: Failed to patch boot images: boot, init_boot, vendor_boot
        2: Failed to load boot image: init_boot
        3: Failed to read boot image data: Boot::V3::ramdisk
        4: failed to fill whole buffer

Changes:

* Remove all uses of #[from] from thiserror-derived error types.
* Errors during parsing and serialization of RSA private keys, RSA
  public keys, and X509 certificates now include the file path.
* Removed unnecessary uses of BufReader and BufWriter when reading and
  writing RSA private keys, RSA public keys, and X509 certificates,
  since they need to be fully read into memory anyway.
* Use ReadFixedSizeExt instead of read_exact() where possible.
* Use &'static str instead of String in error fields where all possible
  values are known at compile-time to avoid unnecessary heap allocation.
* Use ok_or() instead of ok_or_else() to construct errors when the error
  variant uses known data and does not require heap allocation.
* Using DebugString instead of String in error variants that store a
  preformatted debug string.

While working on this commit, an unrelated bug was found and fixed:

* Fix vendor v4 boot images that were truncated within the padding
  following the bootconfig section being treated as valid.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 02:34:17 -05:00
Andrew Gunnerson 6de5cb783a CHANGELOG.md: Add entry for PR #398
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 22:09:03 -05:00
Andrew Gunnerson 12d6f7f78c util: Wrap std's range types instead of inventing our own
This makes the type potentially more useful outside of just our current
use case of just error messages. Ranges where the starting value is
exclusive are no longer supported, but weren't used anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 21:56:13 -05:00
Andrew Gunnerson 3c0a77df21 CHANGELOG.md: Add entry for PR #397
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 21:28:20 -05:00
Andrew Gunnerson d1b6bce227 Update bzip2-rs to 0.5.0 and switch to Rust backend implementation
The bzip2-rs library now has a new maintainer (same folks that maintain
the sudo-rs project). Version 0.5.0 was recently released, which
includes some much needed bug fixes, but is missing one final one, so we
still need to keep our fork for now.

This commit also switches the backend implementation from the official C
implementation to a Rust implementation maintained by the bzip2-rs
folks. This leaves xz as the only C dependency remaining in avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 20:23:40 -05:00
Andrew Gunnerson 10e0748d18 CHANGELOG.md: Add entry for PR #395
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 03:58:28 -05:00
Andrew Gunnerson 9cf63c7036 Treat Magisk versions newer than latest as supporting all features
Previously, when patching with a Magisk version newer than the latest
supported version and using --ignore-magisk-warnings, the upper bound of
VER_PREINIT_DEVICE and VER_XZ_BACKUP would prevent those features from
being used. It makes more sense to assume that a newer version supports
all the same features as the latest supported version instead.

Issue: #393

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 03:53:11 -05:00
Andrew Gunnerson 291b3c887b CHANGELOG.md: Add entry for PR #394
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 00:01:39 -05:00
Andrew Gunnerson 5d7eb13fbc Fix crash when ignoring warning about missing preinit device
MagiskRootPatcher was previously assuming that there is a preinit device
value if the Magisk version requires it.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-10 23:58:36 -05:00
Andrew Gunnerson 05cd74719e Version 3.10.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 19:20:39 -05:00
Andrew Gunnerson 7e0d5584d9 CHANGELOG.md: Add entry for PR #392
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 19:01:08 -05:00
Andrew Gunnerson b351d47f28 Update dependencies and fix most pedantic clippy warnings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 18:46:40 -05:00
Andrew Gunnerson 421b0a5207 CHANGELOG.md: Add entry for PR #391
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 17:14:21 -05:00
Andrew Gunnerson cad08a6a2f Add support for Magisk 28100
There is nothing new that breaks compatibility with avbroot.

Fixes: #389

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:21:05 -05:00
Andrew Gunnerson 0a32dfaec3 CHANGELOG.md: Add entry for PR #390
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:19:46 -05:00
Andrew Gunnerson b03b2ade6a Improve parser error messages
* Remove the ReadString trait and perform UTF-8 conversions explicitly.
  UTF-8 conversion errors are now individual errors instead of being
  hidden in an Io error.
* Add new IntOutOfBounds errors that include the actual field value and
  the range of values that are valid for the field. Integer casting also
  uses this error type where possible.
* Replace remaining uses of FieldOutOfBounds with IntOverflow, which is
  returned when checked arithmetic fails. For code simplicity, it does
  not store intermediate values.
* Replace ReadFieldError and WriteFieldError with the generic Io error
  to simplify the code. They were used inconsistently anyway.
* Use fully qualified field names in avb and bootimage error messages
  since the same name frequently appears in multiple structs.
* Replace InvalidFieldValue with more specific errors in bootimage.
* Replace all stringly-typed errors in lp and sparse with (very)
  specific errors.
* Fix the wording in a number of errors.
* Add new ReadFixedSizeExt trait for reading fixed size arrays and vecs
  from Read types.
* Remove some fallible casts from u32 to usize and from usize to u64.
  avbroot only supports 32-bit and 64-bit systems anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:08:28 -05:00
Andrew Gunnerson c198646c7c Add documentation for Android 16's developer switch for 16K page size kernels
avbroot will not support patching these internal OTAs because it
requires modifying filesystems and creating incremental OTAs. However,
it should be possible to do this manually if someone really wants to try
out a 16K page size kernel.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-06 18:09:34 -05:00
Andrew Gunnerson d815a78e9a CHANGELOG.md: Add entry for PR #386
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:47:31 -05:00
Andrew Gunnerson 99c5800f96 Update dependencies and pin Github Actions actions to specific commits
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:42:35 -05:00
Andrew Gunnerson c801fbb069 CHANGELOG.md: Add entry for PR #385
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:37:54 -05:00
Andrew Gunnerson 0b250086a3 protobuf: Update update_metadata.proto from AOSP
Neither of the two new fields need any special handling in avbroot. This
just allows us to preserve the values from the original OTA.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:33:34 -05:00
Andrew Gunnerson f6c6c8ab40 CHANGELOG.md: Add entry for PR #384
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:27:38 -05:00
Andrew Gunnerson 5c353a5f42 Remove unused byteorder crate
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:57 -05:00
Andrew Gunnerson ad932ed445 sparse: Switch to read_from_io()/write_to_io()
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:44 -05:00
Andrew Gunnerson d04d5451dc stream: Remove unused padded string functions
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:30 -05:00
Andrew Gunnerson 1d5db9f731 bootimage: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:28 -05:00
Andrew Gunnerson 409b9298a5 avb: Switch to zerocopy
This commit also fixes a bug where avb::Header::release_string was
allowed to take the full 48-bytes, which was incorrect because libavb
expects the field to be NULL-terminated. This was not a problem in
practice because the release string is usually short and even if it
wasn't, the 80 reserved bytes that immediately follow it are all zeros.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:01:43 -05:00
Andrew Gunnerson 516238d907 hashtree: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:59:56 -05:00
Andrew Gunnerson 39d5fbf76d payload: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:58:35 -05:00
Andrew Gunnerson 20f5bdacc7 compression: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:58:08 -05:00
Andrew Gunnerson 5b8eefa867 fec: Switch to zerocopy
This commit also adds support for parsing FEC images with unknown extra
custom fields.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:57:55 -05:00
Andrew Gunnerson b2610fa0e9 Update zerocopy to 0.8.11
>=0.8.10 is needed for the new read_from_io()/write_to_io() helper
functions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:57:39 -05:00
Andrew Gunnerson 8a5550a43c Add new traits for zero padding and unpadding strings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:56:55 -05:00
Ivan Katrovsky e16f4c7fa7 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/10c425dedea262678fda3c0c097b82fdf4352f08
2024-11-13 17:54:14 +03:00
Andrew Gunnerson 983e6c40a5 Version 3.9.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 20:04:59 -05:00
Andrew Gunnerson 8729854727 CHANGELOG.md: Add entry for PR #377
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:55:44 -05:00
Andrew Gunnerson 0d1beb7734 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:54:50 -05:00
Andrew Gunnerson 2cd0d69238 CHANGELOG.md: Add entry for PR #376
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:43:50 -05:00
Andrew Gunnerson c0e65264e5 sparse: Allow parsing files with unknown fields
The official AOSP implementation does, so we should too. This allows
unpacking Samsung's sparse images, which have an extra 4 bytes in both
the file header and chunk headers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:41:27 -05:00
Andrew Gunnerson 37b15a2b6a CHANGELOG.md: Add entry for PR #374
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:38:56 -05:00
Andrew Gunnerson e4994fbe98 format/payload: Allow verifying signatures without an unpadded size
Older OTAs created before the payload metadata format supported EC
signatures will not have the `unpadded_signature_size` field set. In
this case, we'll just use the full length of `data`, which is what
update_engine also does.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:38:28 -05:00
Andrew Gunnerson cf1bacab30 CHANGELOG.md: Add entry for PR #373
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:27:24 -05:00
Andrew Gunnerson 4832121160 format/ota: Allow verifying OTAs without metadata.pb
While patched OTAs produced by avbroot always include the protobuf
version of the OTA metadata, the original OTA may not. Verifying those
with `avbroot ota verify` is a valid use case.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:25:00 -05:00
Andrew Gunnerson c614e61744 CHANGELOG.md: Add entry for PR #371
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-07 22:13:45 -05:00
Andrew Gunnerson 9ebce1666c Add option to skip verifying recovery's OTA cert too
Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-07 22:04:11 -05:00
Andrew Gunnerson fb34198ffc CHANGELOG.md: Add entry for PR #370
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 18:00:13 -05:00
Andrew Gunnerson 4a5eab4ba0 crypto: Fix minor clippy warning
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:59:26 -05:00
Andrew Gunnerson 1e1818ad8f CHANGELOG.md: Add entry for PR #369
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:58:59 -05:00
Andrew Gunnerson 3f25ad7c76 Add more context for avb::Header::set_algo_for_key() calls
Previously, it was not always obvious which key was problematic.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:53:01 -05:00
Andrew Gunnerson 37e28eb040 CHANGELOG.md: Add entry for PR #367
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:51:41 -05:00
Andrew Gunnerson ce87757fd1 patch/boot: Avoid loading boot images when there are no patchers
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 09:30:04 -05:00
Andrew Gunnerson 10c425dede Add option to skip inserting OTA cert into recovery image
Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 09:30:03 -05:00
Andrew Gunnerson 8a0d147993 CHANGELOG.md: Add entry for PR #368
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-04 18:54:23 -05:00
Andrew Gunnerson 062aa21485 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-04 18:32:58 -05:00
Andrew Gunnerson 7ffeb5e5cb Version 3.8.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 22:25:18 -04:00
Andrew Gunnerson a9a1aa55e9 CHANGELOG.md: Add entry for PR #364
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 22:18:58 -04:00
Andrew Gunnerson 0ca2872111 Update dependencies
zerocopy 0.8 is the only dependency with breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 22:16:59 -04:00
Andrew Gunnerson a403c26b54 CHANGELOG.md: Add entry for PR #363
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 21:36:47 -04:00
Andrew Gunnerson 4afa1d1ba6 cli/avb: Add subcommand for computing vbmeta digest
This computes the special SHA256 digest that is equal to the
ro.boot.vbmeta.digest property value on a real device.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 21:29:04 -04:00
Andrew Gunnerson 9f669bc53e Version 3.7.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-10 18:12:48 -04:00
Andrew Gunnerson fc89159f09 CHANGELOG.md: Add entry for PR #362
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-10 18:04:25 -04:00
Andrew Gunnerson 18f43a0e57 Add support for Magisk 28000
There is nothing new that isn't already supported.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-10 18:03:04 -04:00
Andrew Gunnerson af7262a0d2 cli/sparse: Fix clippy warning on non-Linux/Android builds
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-22 21:21:11 -04:00
Andrew Gunnerson 5a7885674e ci.yml: Use target name in Rust cache key
Otherwise, the x86_64-unknown-linux-gnu build running on Ubuntu 22.04
can use the cache from the aarch64-linux-android31 build that originally
ran on Ubuntu 24.04. This fails due to some cached components having
been compiled against a newer version of glibc.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-22 21:13:33 -04:00
Andrew Gunnerson c974ab5ec3 Use Rust conventional comment style for license headers
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-22 14:58:01 -04:00
Andrew Gunnerson 02ae9cd0e3 Version 3.7.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-21 18:42:42 -04:00
Andrew Gunnerson 9c92d32a80 CHANGELOG.md: Add entry for PR #357
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-21 18:42:19 -04:00
Andrew Gunnerson 296df25fb9 Ensure that PrepatchedImagePatcher runs before other patchers
On older devices, like the Pixel 4a, where `boot` is used for both
Android and recovery mode, the image will be patched by OtaCertPatcher
and PrepatchedImagePatcher. OtaCertPatcher was always set to run first,
so when PrepatchedImagePatcher used the user-supplied image as-is, prior
modifications got wiped out. This made is so users could no longer flash
further patched OTAs.

This is an unfortunate regression that was introduced in avbroot 2.0.0.
The e2e tests never caught this issue because the --prepatched test was
being fed the boot image previously patched by --magisk. That already
had valid certs so the result of OtaCertPatcher's modifications being
lost were not visible. This commit also fixes the e2e tests so that this
type of issue will be caught in the future.

Fixes: #356

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-21 18:17:46 -04:00
Andrew Gunnerson 352352b4d4 CHANGELOG.md: Add entry for PR #355
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 20:06:57 -04:00
Andrew Gunnerson e28ef42c0e Switch to passterm library for password prompts
This library behaves exactly the same as rpassword, but has zero
dependencies so we no longer need to pull in the ancient windows-sys
0.48.x libraries.

We still transitively depend on both windows-sys 0.52.x and 0.59.x
though.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 17:46:52 -04:00
Andrew Gunnerson 5775c86e5b CHANGELOG.md: Add entry for PR #354
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 16:19:31 -04:00
Andrew Gunnerson 3dc8e07c0f cli/payload: Allow inspecting delta payloads
The pack and unpack commands can never support delta payloads, but
there's no reason not to allow the repack and info commands to read
them.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 16:12:17 -04:00
Andrew Gunnerson 040dcd1a5c Return ExitCode from main
std::process::exit() calls the exit syscall, which doesn't run
destructors. It doesn't matter for avbroot, but better to use ExitCode
anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-01 22:36:41 -04:00
Andrew Gunnerson 41a578975f CHANGELOG.md: Add entry for PR #347
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-01 02:23:05 -04:00
Andrew Gunnerson e9638d25b0 Add support for packing and unpacking Android sparse images
This supports all features of Android sparse images, including holes,
and CRC32 (both full image checksum and CRC32 chunks).

Partial sparse images, like those included in GrapheneOS' new optimized
factory images, can also be packed and unpacked with these new commands,
unlike AOSP's simg2img and img2simg tools.

This new functionality is not relevant for avbroot's main use case, but
is useful for unpacking certain factory images for comparison with OTAs
during troubleshooting.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-01 02:14:11 -04:00
Andrew Gunnerson e25080ddef Merge pull request #346 from bugreportion/patch-1
README.ru.md: update translation
2024-08-31 12:43:48 -04:00
Ivan Katrovsky 0e7d778cf5 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/8e52a9cf8c6f3bd6ea281d93364ac3cb9eddd806
* https://github.com/chenxiaolong/avbroot/commit/4a1dab40694b50cc4b914ff9bbfc869344afed9c
* https://github.com/chenxiaolong/avbroot/commit/24320d4fae309cd4f19a8ccff0ab923905d04e8f
* https://github.com/chenxiaolong/avbroot/commit/7113fb32efc9d1db5c76529d28317fcd3e42ebcb
* https://github.com/chenxiaolong/avbroot/commit/8ca1a289a8111da76ac0cc25ec64fdb2b051f643

Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-08-31 02:52:50 +03:00
Andrew Gunnerson 8ca1a289a8 README.md: Split setting ANDROID_PRODUCT_OUT env var to separate step
Also add examples for powershell and cmd instead of assuming bash
syntax.

Fixes: #340

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-30 19:12:56 -04:00
Andrew Gunnerson dfbc2f807f CHANGELOG.md: Add entry for PR #343
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-28 18:37:45 -04:00
Andrew Gunnerson e55cf3d679 lp: Sparse files are not required when packing and unpacking empty images
Empty images don't contain any extent metadata so the partition sizes
are just discarded.

Don't write parsers when you're tired, folks!

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-28 18:30:57 -04:00
Andrew Gunnerson 7c38c5609b CHANGELOG.md: Add entry for PR #342
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-27 23:57:59 -04:00
Andrew Gunnerson 13c910274e Add support for packing and unpacking logical partition images
This supports both empty and normal LP images, including those that span
multiple files/devices.

Currently, repacked files are semantically equivalent, but not exactly
identical. avbroot's data structure for the metadata does not preserve
the arbitrary partition ordering of the LP image. Instead, to make the
API a bit nicer, it only preserves the relative partition ordering
within partition groups.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-27 23:32:29 -04:00
Andrew Gunnerson dd40f64918 CHANGELOG.md: Add entry for PR #337
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-20 21:37:57 -04:00
Andrew Gunnerson 7113fb32ef Add support for outputting zip files with no data descriptors
This commit adds a new `--zip-mode` parameter to `avbroot ota patch` to
control whether the patched OTA zip is written with data descriptors or
not. By default, the `streaming` mode is used, which matches the current
behavior where the zip is hashed for signing as it is being written. The
new `seekable` mode fully writes the zip before rereading it to hash the
contents.

The new mode is useful for devices with broken zip parsers that fail to
properly handle data descriptors.

All of the end-to-end tests have been duplicated to test both modes.

Adding the seekable mode necessitated a couple other changes:

* BufWriter is no longer used. Type erasure is very painful in Rust, so
  we need to keep the writer types the same for both the streaming and
  seekable modes. BufWriter is unusable in the seekable mode because we
  need to be able to read back what was written, which isn't supported.

* HolePunchingWriter has been removed. It was a simple way to produce
  sparse files by seeking whenever a write buffer consists fully of
  zeros. When combined with BufWriter, there was previously never a
  situation where this was undesirable. However, with the new seekable
  mode and the zip library's pattern of writing one field at a time, the
  final 2 zero bytes (representing an empty archive comment) is never
  written and the file size is not increased either.

  Removing this is not a big deal since we no longer use stripped OTAs
  for the end-to-end tests. Those were really the only OTAs that
  benefitted from sparse files. A real OTA has very few zero bytes due
  to compression.

Issue: #328

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-20 21:31:33 -04:00
Andrew Gunnerson 9c9d656ea2 CHANGELOG.md: Add entry for PR #336
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 15:03:26 -04:00
Andrew Gunnerson e06674a7f0 Print a useful error message when there's no TTY for a password prompt
ENXIO (No such device or address) and ENOTTY (Inappropriate ioctl for
device) are not very user-friendly error messages.

Issue: https://github.com/chenxiaolong/my-avbroot-setup/issues/2

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 14:57:46 -04:00
Andrew Gunnerson cf5ef13e47 Version 3.6.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:38 -04:00
Andrew Gunnerson 5fada419cb CHANGELOG.md: Add entry for PR #335
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:05 -04:00
Andrew Gunnerson eeea9f41b4 cli/args: Use tracing::Level directly
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:58:11 -04:00
Andrew Gunnerson 0bebf120c6 CHANGELOG.md: Add entry for PR #334
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:57:40 -04:00
Andrew Gunnerson 264c602fdb cli/ota: Remove unnecessary mutex
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:49:41 -04:00
Andrew Gunnerson 343e2e279c CHANGELOG.md: Add entry for PR #333
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:44:45 -04:00
Andrew Gunnerson 59ca759262 Add support for gzip VABC algorithm
Older devices, like the Pixel 4a 5G (bramble) use gzip instead of lz4.

This commit also reworks the CoW size estimate calculation to add the
same constant headroom that AOSP's delta_generator adds. Previously,
avbroot was already adding an additional 1% to account for differences
in compression ratios across compression library implementations. This
papered over the issue for large partitions, but small partitions could
still have a CoW size estimate that's too small. Adding the constant
headroom prevents ENOSPC when flashing those partitions.

Fixes: #332

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:33:43 -04:00
Andrew Gunnerson 83ab475c11 Version 3.5.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-15 20:45:04 -04:00
Andrew Gunnerson fe54640029 CHANGELOG.md: Add entry for PR #331
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-15 20:44:32 -04:00
Andrew Gunnerson fec1840a5f Add subcommands for packing and unpacking payload binaries
Some devices have "full" OTAs where the payload is missing the recovery
partition. These subcommands make it possible to manually add back the
missing image. Given the strict requirements for how the OTA zip is laid
out and signed, users can't just replace payload.bin in a zip and call
it a day, but it's sufficient for feeding a modified input to
`avbroot ota patch`.

Issue: #328

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-15 20:38:52 -04:00
Andrew Gunnerson 395f6934ff CHANGELOG.md: Add entry for PR #329
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-14 22:07:47 -04:00
Andrew Gunnerson a83b2fbfa9 Update dependencies
prost and protox have breaking changes, but none that affect avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-14 21:06:21 -04:00
Andrew Gunnerson e18ef20e4d Version 3.4.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-28 18:04:20 -04:00
Andrew Gunnerson b140620ed3 CHANGELOG.md: Add entry for PR #323
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-28 18:03:16 -04:00
Andrew Gunnerson dd9d8959fd Add support for Magisk 27006
Version 27006 now requires init-ld to be included in the ramdisk.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-28 17:58:13 -04:00
Andrew Gunnerson 9a7cece973 CHANGELOG.md: Add entry for PR #321
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-15 21:44:29 -04:00
Andrew Gunnerson aac3aded78 Update all dependencies
Version 1.6.0 of the bytes library was yanked. There's no vulnerability
that impacts avbroot, but it was tripping the cargo-deny checks.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-15 21:32:14 -04:00
Andrew Gunnerson 24320d4fae README.md: Move signature verification instructions to shared repo
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-14 18:23:55 -04:00
Ivan Katrovsky 6800ae073e README.ru.md: update translation
Sync with https://github.com/chenxiaolong/avbroot/commit/4d90ee2ac6223ec9e4ca1b1b77d84973ad90cd75

Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-07-06 02:37:41 -04:00
Andrew Gunnerson 4d90ee2ac6 README.md: Document that uninstalling Magisk will also flash an unsigned boot image
Closes: #318

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-05 19:45:27 -04:00
Ivan Katrovsky 6b087c0844 README.ru.md: update translation
Sync with https://github.com/chenxiaolong/avbroot/commit/d384a8a99bda591a6299f8f76f693eeb1725cb0f

Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-07-05 15:46:26 +03:00
Andrew Gunnerson d384a8a99b README.md: Document that fastboot >=34 is required
Older versions of fastboot have bugs that cause the reboot to fastbootd
mode to be skipped, causing failures when flashing dynamic partitions.

Fixes: #314

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-04 16:20:22 -04:00
Andrew Gunnerson bc358c62af Version 3.4.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:52:44 -04:00
Andrew Gunnerson fc05cb901a CHANGELOG.md: Add entry for PR #312
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:52:11 -04:00
Andrew Gunnerson 031ac8aa31 cli/avb: Add --public-key option for extract-avb subcommand
Issue: #312

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:46:24 -04:00
Andrew Gunnerson 4a1dab4069 Add support for signing with an external program
By default, the helper program is invoked in a way that is compatible
with avbtool's --signing_helper. However, the arguments have been
extended slightly to allow passing in the passphrase file or environment
variable for non-interactive use.

Fixes: #310

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:46:22 -04:00
Andrew Gunnerson bf885e40cf CHANGELOG.md: Add entry for PR #311
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-24 18:15:49 -04:00
Andrew Gunnerson c2a441cf78 avb: AlgorithmType: Fail on unknown key types
This is never reachable due to additional checks in every code path that
invokes sign() and verify(), but still better to be correct.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-24 17:54:50 -04:00
Andrew Gunnerson f6c6a9509a Version 3.3.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-23 15:51:57 -04:00
Andrew Gunnerson 5a00995366 CHANGELOG.md: Add entry for PR #309
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-22 18:47:28 -04:00
Andrew Gunnerson edf537aad6 Add payload subcommand for dumping payload.bin header
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-22 18:42:46 -04:00
Andrew Gunnerson f006f20209 CHANGELOG.md: Add entry for PR #307
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-22 18:38:44 -04:00
Andrew Gunnerson f36c1ca451 payload: Fudge CoW size estimate by 1%
lz4_flex appears to compress system images better than the original lz4
implementation used in libsnapshot_cow, so the estimates are too low.

Fixes: #306

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-21 22:36:41 -04:00
Andrew Gunnerson 29b72961a3 Update CoW size estimate when replacing entire dynamic partitions
Otherwise, if the partition size increases or the data becomes more
incompressible, update_engine might fail to flash the partition due to
the CoW block device running out of space.

Since all known VABC-enabled OTAs in the wild currently use CoW v2 with
lz4 compression, this is the only configuration we support. CoW v3 also
exists in AOSP's libsnapshot_cow, but is much more complicated to
implement and is not yet used, even in the Android 15 beta OTAs.

Fixes: #306

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

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

Issue: #301

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

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

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

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

Fixes: #294

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

Issue: #290

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

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

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

Closes: #286

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

Fixes: #285

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

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

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

Closes: #265

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

Fixes: #252

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

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

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

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

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

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

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

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

Fixes: #252

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

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

Closes: #235

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

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-30 19:16:05 -05:00
Andrew Gunnerson 833ca931e8 Version 3.0.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:57:22 -05:00
Andrew Gunnerson 9b6677d28a CHANGELOG.md: Remove entry for PR #220
It was obsoleted by #226.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:55:06 -05:00
Andrew Gunnerson 12e59fcf53 CHANGELOG.md: Add entry for PR #245
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:52:30 -05:00
Andrew Gunnerson 4bc7363f70 Merge pull request #245 from chenxiaolong/deps
Update all dependencies
2023-12-29 20:51:53 -05:00
Andrew Gunnerson b5ad9dddf6 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:44:12 -05:00
Andrew Gunnerson f0aba89549 CHANGELOG.md: Add entry for PR #243
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:41:22 -05:00
Andrew Gunnerson c9633682ef Merge pull request #243 from chenxiaolong/repair_mode
README.md: Document Repair Mode
2023-12-29 20:39:22 -05:00
Andrew Gunnerson da17935972 CHANGELOG.md: Add entry for PR #241
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 16:18:39 -05:00
Andrew Gunnerson c2140a85e9 Merge pull request #241 from chenxiaolong/e2e
e2e: Switch to using mock OTAs for testing
2023-12-29 16:17:31 -05:00
Andrew Gunnerson 3a3582ce4c e2e: Switch to using mock OTAs for testing
This commit replaces the previous approach of patching real OTAs with
patching mock OTAs. The motivation for this change is to make it
possible to test the system partition otacerts.zip patching without
needing to download huge files. Adding the system image to the stripped
OTAs would increase the file size by an order of magnitude.

The mock OTAs are generated from a set of profiles defined in e2e.toml.
The four included profiles are meant to mimic the OTAs used for testing
before:

* pixel_v4_gki     ~= cheetah
* pixel_v4_non_gki ~= bluejay
* pixel_v3         ~= bramble
* pixel_v2         ~= sunfish

There is no equivalent profile for ossi because newer OnePlus devices no
longer support custom signing keys properly.

The mock OTAs are perfectly valid, structure and signature-wise. They
just don't include any real partition data where possible. They are
initially signed with a different set of keys to ensure that the changes
made by the patching process are actually visible.

With how small the mock OTAs are, testing every profile only takes about
two seconds. Thus, the Github Actions workflow was adjusted to just run
e2e in the same job as the build.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 16:07:57 -05:00
Andrew Gunnerson b029116a5a CHANGELOG.md: Add entry for PR #244
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 15:48:12 -05:00
Andrew Gunnerson af1b01646d Merge pull request #244 from chenxiaolong/system_patch_performance
Optimize repacking of modified system image into payload
2023-12-29 15:47:09 -05:00
Andrew Gunnerson e85102709f Optimize repacking of modified system image into payload
When the original payload extents are all in order and have no gaps, we
can efficiently copy unmodified chunks of the system image from the
original payload into the new payload. Only the chunks containing the
modified regions (`otacerts.zip`, hash tree, FEC data, and AVB metadata)
need to be recompressed. This massively reduces the CPU usage since
usually only <20 MiB need to be recompressed.

If the conditions for the optimized path aren't satisfied (eg. extents
aren't in order or `--replace` is used), then it falls back to splitting
and compressing the entire system image.

This fixes the performance issues introduced in #240. This is probably
the best that we can do given that we now always patch the system image.

Issue: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 00:02:35 -05:00
Andrew Gunnerson 124629dc45 README.md: Document Repair Mode
Closes: #216

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-28 18:10:26 -05:00
Andrew Gunnerson e89c4bdc9e CHANGELOG.md: Add entry for PR #240
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-28 18:00:23 -05:00
Andrew Gunnerson c1095b8321 Merge pull request #240 from chenxiaolong/system_otacerts
Add support for replacing otacerts.zip in the system image
2023-12-28 17:49:02 -05:00
Andrew Gunnerson 17504a7f81 Add support for replacing otacerts.zip in the system image
Previously, overriding otacerts.zip in the system partition required the
user to flash a Magisk/KernelSU module that would bind mount over the
file during boot. While this worked well enough, it's insufficient for
unrooted setups, which has become more important since unrooting is the
only safe way to use the new OEM repair mode feature. With the stock
otacerts.zip, the OEM's default OTA updater app could run and install an
OS upgrade that's not signed by the user's key.

With this commit, the raw otacerts.zip bytes in the system partition are
directly replaced with a new zip that contains the user's certificate.
This method was inspired by @pascallj's comment in #216 suggesting
intentionally corrupting the otacerts.zip data in the filesystem.

Because avbroot does not have filesystem parsers for ext4/f2fs/erofs, we
rely on a heuristic-based search on the raw filesystem image. The file
is always smaller than one block (which is at least 4096 bytes on all
known devices), so the file data is stored contiguously on disk and in
the case of erofs, won't be compressed. None of the three filesystems
are copy-on-write and thus, have no filesystem-level data checksums. For
the dm-verity layer one level up, avbroot already knows how to recompute
the hash tree and FEC data.

To ensure that there are no false positives, any match that the search
finds must correctly parse as a valid zip and every entry within the zip
must have a filename that ends in .x509.pem. This matches what
update_engine expects from a proper otacerts.zip file.

Since the new approach is doing a raw search and replace, the old and
new files must have the same size. When the new zip is smaller, null
bytes are added to the zip archive comment field to pad to the correct
size. When the new zip is larger, avbroot will attempt the following to
try and make the file size smaller:

1. Enable zip deflate compression
2. Strip the X.509 signature from the certificate
3. Clear out the issuer RDN sequence from the certificate
4. Clear out the subject RDN sequence from the certificate

The latter three changes work because Android never performs any PKI
operations with the certificate. There is no CA certificate chain. The
X.509 certificate file is nothing more than a way to transport an RSA
public key.

avbroot requires the user's key to be RSA 4096. If the original zip had
the same key size, then none of these shrinking methods are needed. If
it contained an RSA 2048 key, then the first two modifications are
usually sufficient. The latter two modifications should only be needed
if the user picked a really long subject value when generating the
certificate.

With these new changes, the OTA patching time will approximately double
on a system with an SSD and modern CPU. This is dominated by the time it
takes to XZ-compress the system partition image. The compression is
already parallelized and scales linearly with the number of cores.
There's likely not much more that can be done to further speed this up.

Finally, these new changes are currently excluded from the e2e tests
because including the system partition in the stripped OTAs would
increase the file size by an order of magnitude. This could potentially
be solved in the future by generating our own small OTAs to use for
testing instead of running against real device OTAs.

Fixes: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-24 18:23:03 -05:00
Andrew Gunnerson d8190143bf CHANGELOG.md: Add entry for PR #237
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-22 16:17:06 -05:00
Andrew Gunnerson 0944abcea2 Merge pull request #237 from chenxiaolong/autodetect
Allow boot image autodetection to inspect boot images
2023-12-22 16:14:52 -05:00
Andrew Gunnerson 249c36d8c3 Allow boot image autodetection to inspect boot images
During patching, all boot images are now extracted and the individual
patchers can inspect them to determine which ones need modifications.
This replaces the previous mechanism of detecting which boot images to
patch based on the name alone.

With this new method, the --boot-partition and --otacerts-partitions
options are no longer needed. The former option is kept (but ignored
with a warning message) for backwards compatibility, but the latter is
completely removed because it never made it to a stable release.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-22 00:35:32 -05:00
Andrew Gunnerson 446e87289f CHANGELOG.md: Add entry for PR #234
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 22:14:44 -05:00
Andrew Gunnerson 04b8e3c315 Merge pull request #234 from chenxiaolong/fec_param_order
fec: Change update() parameter order to be more consistent
2023-12-17 22:14:09 -05:00
Andrew Gunnerson 03809a1e88 fec: Change update() parameter order to be more consistent
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 22:07:00 -05:00
Andrew Gunnerson 1fcb29532d CHANGELOG.md: Add entry for PR #233
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 21:08:28 -05:00
Andrew Gunnerson b383c8a0ab Merge pull request #233 from chenxiaolong/hash_tree
Split dm-verify hash tree logic out of avb module
2023-12-17 21:06:07 -05:00
Andrew Gunnerson 7ea8e8fa9e Split dm-verify hash tree logic out of avb module
* Refactor hash tree computation to work on a preallocated hash tree
  buffer. This makes it possible to partially update a hash tree, which
  is now supported.

* Add new subcommands for working with hash trees. There's no standard
  header format for dm-verity information, so these commands write hash
  tree files with a custom header. The commands are not really useful
  outside of debugging avbroot's hash tree implementation.

  Using AVB was considered, but it has no support for the hash tree data
  living in a separate file from the input. If other parties agree on a
  standard header in the future, avbroot will switch to that format.

* Add tests for the hash tree implementation.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 20:48:00 -05:00
Andrew Gunnerson ce43b3c657 CHANGELOG.md: Add entry for PR #232
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 15:08:43 -05:00
Andrew Gunnerson 05e5e59d65 Merge pull request #232 from chenxiaolong/small_hash_tree
avb: hashtree: Fix incorrect I/O read size for input smaller than one block
2023-12-17 15:07:51 -05:00
Andrew Gunnerson 72b8120bf9 avb: hashtree: Fix incorrect I/O read size for input smaller than one block
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 15:00:50 -05:00
Andrew Gunnerson 8110f82b17 CHANGELOG.md: Add entry for PR #231
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 14:59:47 -05:00
Andrew Gunnerson 64dbfa241e Merge pull request #231 from chenxiaolong/overlap
util: merge_overlapping: Skip empty and invalid ranges
2023-12-17 14:59:11 -05:00
Andrew Gunnerson 7041c8ef2c util: merge_overlapping: Skip empty and invalid ranges
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 14:50:21 -05:00
Andrew Gunnerson 7296eed51f CHANGELOG.md: Add entry for PR #230
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 00:48:36 -05:00
Andrew Gunnerson 698810d256 Merge pull request #230 from chenxiaolong/fec_update
fec: Add support for partially updating FEC data
2023-12-17 00:47:45 -05:00
Andrew Gunnerson acf57272a7 fec: Add support for partially updating FEC data
FEC data can be updated efficiently with "round" granularity when the
regions where the input file was modified are known.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 00:39:04 -05:00
Andrew Gunnerson 2b50e4527f CHANGELOG.md: Add entry for PR #229
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 23:09:09 -05:00
Andrew Gunnerson cd7b5ab23c Merge pull request #229 from chenxiaolong/partition_hashes
cli/ota: Also verify whole-partition hashes
2023-12-16 23:07:45 -05:00
Andrew Gunnerson f73c451d23 cli/ota: Also verify whole-partition hashes
Previously, when running `avbroot ota verify`, only the payload chunk
hashes were verified.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:59:33 -05:00
Andrew Gunnerson a801e18595 CHANGELOG.md: Add entry for PR #228
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:51:06 -05:00
Andrew Gunnerson 3932ff9092 Merge pull request #228 from chenxiaolong/parallel_compress
Split new partition images into chunks and compress in parallel
2023-12-16 22:49:22 -05:00
Andrew Gunnerson 1a36fbd46c Split new partition images into chunks and compress in parallel
Previously, all replacement partition images (those that have been
patched or `--replace`d) were compressed as a whole, which would be very
slow for larger images. Instead, we'll split the images into 2 MiB
chunks and compress them in parallel. This more closely matches what
AOSP's payload_generator does and scales linearly with the number of CPU
cores. The compression is less efficient, but the file size generally
only increases by 10s of KiB.

This commit also reworks the implementation so that patched images are
stored in temp files instead of in memory again. In hindsight, doing
everything in memory only made things more complex and causes the memory
usage to blow up when doing things like `--replace system <path>`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:38:18 -05:00
Andrew Gunnerson 077a80f4ce CHANGELOG.md: Add entry for PR #226
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:41:30 -05:00
Andrew Gunnerson 1e43930f3c Merge pull request #226 from chenxiaolong/critical
cli/ota: Limit critical partition check to bootloader-verified partitions
2023-12-12 21:39:39 -05:00
Andrew Gunnerson d00e53fb2a cli/ota: Limit critical partition check to bootloader-verified partitions
dm-verity partitions don't necessarily have AVB descriptors inside the
root vbmeta images. The fstab might verify them against a public key on
disk using the `avb_keys` fs_mgr option. We have no way to statically
check for this scenario and it's becoming increasingly common on newer
OnePlus devices. Instead, we'll just limit the checks to partitions that
are verified by the bootloader.

Fixes: #223

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:30:24 -05:00
Andrew Gunnerson 0b5f3b30cf CHANGELOG.md: Add entry for PR #227
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:30:09 -05:00
Andrew Gunnerson 10c2a0969f Merge pull request #227 from chenxiaolong/symlink
workflows/ci: Fix overwriting symlink for target/output
2023-12-12 21:29:25 -05:00
Andrew Gunnerson 83218a747d workflows/ci: Fix overwriting symlink for target/output
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:19:26 -05:00
Andrew Gunnerson 6fb7d568cd CHANGELOG.md: Add entry for PR #224
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 19:12:11 -05:00
Andrew Gunnerson 6cbf690e17 Merge pull request #224 from chenxiaolong/static
workflows/ci: Build statically linked executables where possible
2023-12-12 19:07:02 -05:00
Andrew Gunnerson e097f98404 workflows/ci: Build statically linked executables where possible
This way, the precompiled executables can run on distros that ship an
older version of glibc or don't use glibc at all.

Fixes: #222

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 18:34:02 -05:00
Andrew Gunnerson d5605eabc0 workflows/ci: Use Rust triple instead of LLVM triple
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 18:05:00 -05:00
Andrew Gunnerson d1bcf2da80 CHANGELOG.md: Add entry for PR #221
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-11 18:23:49 -05:00
Andrew Gunnerson bbf8e28a28 Merge pull request #221 from chenxiaolong/otacerts
cli/ota: Add new --otacerts-partition option for the patch subcommand
2023-12-11 18:22:03 -05:00
Andrew Gunnerson dc93b17888 cli/ota: Add new --otacerts-partition option
The autodetection logic for `@otacerts` is based on the presence of the
`recovery`, `vendor_boot`, and `boot` partitions (in that order). Some
devices have `vendor_boot`, but put `system/etc/security/otacerts.zip`
inside `boot`.

With the way things are written now, we don't have the ability to
inspect the actual partition images for the autodetection. It is based
on the name only. So, for now, we'll just allow the user to override the
autodetected partition similar to what we already do with the
`--boot-partition` option.

Issue: #218

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 23:32:14 -05:00
Andrew Gunnerson f83a408dc4 CHANGELOG.md: Add entry for PR #220
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 16:03:08 -05:00
Andrew Gunnerson 798fddc4d5 Merge pull request #220 from chenxiaolong/odm
cli/ota: Fine tune `is_critical_to_avb()` for odm partition
2023-12-10 16:01:43 -05:00
Andrew Gunnerson dfed06dcf8 cli/ota: Fine tune is_critical_to_avb() for odm partition
AOSP primarily cares about `odm` and `odm_dlkm`, so update the logic to
check for that specifically. This fixes patching some OTAs that include
an unprotected OEM-specific `odm_ext` image.

Issue: #218

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 15:54:02 -05:00
Andrew Gunnerson 02fd92a640 CHANGELOG.md: Add entry for PR #219
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 13:47:33 -05:00
Andrew Gunnerson dd4faf72ae Merge pull request #219 from chenxiaolong/decode-avb
key: Add new decode-avb subcommand
2023-12-10 13:43:58 -05:00
Andrew Gunnerson adbe249edb key: Add new decode-avb subcommand
This does the reverse of `avbroot key extract-avb`. It's useful for
reconstructing a PKCS8-encoded RSA public key when working with the
`public_key` fields in `avb.toml`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 13:29:45 -05:00
Andrew Gunnerson b7028b13a2 CHANGELOG.md: Add entry for PR #214
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 18:44:11 -05:00
Andrew Gunnerson 8122b0eece Merge pull request #214 from chenxiaolong/RUSTSEC-2023-0071
deny.toml: Temporarily ignore RUSTSEC-2023-0071
2023-12-03 18:41:17 -05:00
Andrew Gunnerson 0613de4ee4 deny.toml: Temporarily ignore RUSTSEC-2023-0071
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.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 18:02:48 -05:00
Andrew Gunnerson 1a18eb7f85 README.md: Document OnePlus boot issues
Issue: #186
Issue: #195
Issue: #212

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 14:58:38 -05:00
Andrew Gunnerson 1e1ca9dcbf CHANGELOG.md: Add entry for PR #211
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:51:39 -05:00
Andrew Gunnerson 6924783f48 Merge pull request #211 from chenxiaolong/lints
Fix new lint warnings from rustc and clippy 1.74.0
2023-11-22 19:50:27 -05:00
Andrew Gunnerson 98071daa33 Fix new lint warnings from rustc and clippy 1.74.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:30:52 -05:00
Andrew Gunnerson 8e1adae947 CHANGELOG.md: Add entry for PR #210
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:29:00 -05:00
Andrew Gunnerson d6705f4f00 Merge pull request #210 from chenxiaolong/avb_2.0_format_1.3
avb: Add support for AVB 2.0 format 1.3.0
2023-11-22 19:27:24 -05:00
Andrew Gunnerson a648df1695 avb: Add support for AVB 2.0 format 1.3.0
Format version 1.3.0 adds a new 32-bit big endian `flags` field to
chain descriptors, carved out from the `reserved` array. There is only
one possible flag, which indicates that the target partition is not A/B
and so the bootloader should not append the `_a` or `_b` suffix.

There are no known AVB images in the wild where the first four bytes of
the `reserved` field are not zero. Thus, to keep the logic simple, the
AVB parser will unconditionally parse those bytes as if they were the
`flags` field, regardless of the file format version.

AOSP changes:

* https://android.googlesource.com/platform/external/avb/+/a1fe228b86543a21739c51352f5ce72f134fccfa%5E%21/
* https://android.googlesource.com/platform/external/avb/+/d00d02c390e267ef43d93562864dd6e45966c435%5E%21/

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 18:47:35 -05:00
Andrew Gunnerson a7c872be3e Version 2.3.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:59:43 -05:00
Andrew Gunnerson cf1ab6ecca CHANGELOG.md: Add entry for PR #208
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:58:53 -05:00
Andrew Gunnerson 7364e8d725 Merge pull request #208 from chenxiaolong/descriptors
cli/ota: Merge property and kernel cmdline descriptors from child into parent
2023-11-15 18:57:26 -05:00
Andrew Gunnerson 935a86e72c cli/ota: Merge property and kernel cmdline descriptors from child into parent
Some devices use the legacy Android/ChromiumOS-specific `dm=` kernel
command line option to configure dm-verity without userspace helpers.
These options are specified in kernel command line descriptors in the
system partition's vbmeta header, which need to be merged into the
parent vbmeta image for the bootloader to see them.

This commit adds support for merging property descriptors and kernel
command line descriptors. Property descriptors are merged based on an
exact string match of the property key. Kernel command line descriptors
are merged based on the (non-empty) text before the first equal sign.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:39:55 -05:00
Andrew Gunnerson a9a6107043 CHANGELOG.md: Add entry for PR #207
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 17:46:49 -05:00
Andrew Gunnerson 3c1d5a8bb3 Merge pull request #207 from chenxiaolong/xz
compression: Add support for XZ-compressed ramdisks
2023-11-15 17:11:23 -05:00
Andrew Gunnerson 26ec8098e5 compression: Add support for XZ-compressed ramdisks
`lineage-20.0-20231109-nightly-taimen-signed.zip` is an example of an
OTA that uses XZ-compressed ramdisks.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 16:37:00 -05:00
Andrew Gunnerson 2a293147b1 Version 2.3.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:19:15 -05:00
Andrew Gunnerson aea12c8d58 CHANGELOG.md: Add entry for PR #206
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:18:43 -05:00
Andrew Gunnerson 5545b0fe1b Merge pull request #206 from chenxiaolong/promote
cli/avb: Promote insecure hash algorithms to secure ones
2023-11-14 19:17:18 -05:00
Andrew Gunnerson 9c818fb165 cli/avb: Promote insecure hash algorithms to secure ones
This is done unconditionally because there shouldn't be any real-world
device that uses AVB2 and has a kernel compiled without sha256 support.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:10:07 -05:00
Andrew Gunnerson d174af8969 CHANGELOG.md: Add entry for PR #205
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 18:54:14 -05:00
Andrew Gunnerson 2cf11094de Merge pull request #205 from chenxiaolong/cmdline
cli/avb: Update kernel cmdline descriptor for devices that use `dm=`
2023-11-14 18:52:16 -05:00
Andrew Gunnerson 6f565969b7 cli/avb: Update kernel cmdline descriptor for devices that use dm=
Older Pixel devices (and ChromiumOS) specify the dm-verity options on
the kernel command line using a custom `dm=` parameter instead of using
dm-init or a userspace helper. This commit updates the avb pack and
repack commands to automatically update the relevant kernel command line
descriptor if it exists.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 18:36:07 -05:00
Andrew Gunnerson da2eb6b717 Merge pull request #204 from chenxiaolong/readme
README.md: Make sections easier to follow
2023-11-13 15:50:01 -05:00
Andrew Gunnerson 2f8d0264eb README.md: Make sections easier to follow
* Split out requirements from the warnings/caveats section and move it
  to the top. Hopefully this helps new users determine whether they can
  even use avbroot quicker.
* Split usage section into separate usage, initial install, and updates
  sections. This should hopefully make the steps much easier to follow
  without a bunch of steps being prefixed with `[Initial setup only]`.
* Explicitly state what arguments are required for Magisk, KernelSU, and
  unrooted setups instead of directing users to the advanced usage
  section.
* Don't assume that everyone will use Magisk in the other sections.
* Explicitly state the assumption that the device should already running
  the OS build that the user wants to patch during initial install.
* Add step to updates section for installing an updated Magisk or
  KernelSU app.
* Move command for building the modules from the modules section to the
  building from source section.
* Update sample error message in the clear vbmeta flags section to match
  what avbroot will actually print out.
* General rewording to try and make things clearer (especially reducing
  parenthesized parts).

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-13 03:51:17 -05:00
Andrew Gunnerson ec1fe74900 CHANGELOG.md: Add entry for PR #202
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-12 18:53:13 -05:00
Andrew Gunnerson b3b7c7b738 Merge pull request #202 from chenxiaolong/error
cli/ota: Improve error reporting when descriptor types are mismatched
2023-11-12 18:51:56 -05:00
Andrew Gunnerson 2f1ee1ae4f cli/ota: Improve error reporting when descriptor types are mismatched
Issue: #201

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-12 18:32:55 -05:00
Andrew Gunnerson cb62996b9f Version 2.3.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:59:49 -05:00
Andrew Gunnerson 54c06835c5 CHANGELOG.md: Add entry for PR #199
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:59:16 -05:00
Andrew Gunnerson 65a1b80da5 Merge pull request #199 from chenxiaolong/magisk_26.4
boot.rs: Bump Magisk version upper bound to 26500
2023-11-06 16:58:50 -05:00
Andrew Gunnerson 76ff1ddcda boot.rs: Bump Magisk version upper bound to 26500
There are no upstream changes that impact avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:40:14 -05:00
Andrew Gunnerson 0a4dda14cd README.md: Clarify that dmesg step should be run on the device
Fixes: #198

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-29 14:02:57 -04:00
Andrew Gunnerson bde8dbfbcd Version 2.3.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 18:10:51 -04:00
Andrew Gunnerson fbafbafe90 CHANGELOG.md: Fix typo: Reword -> Rework
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 18:03:51 -04:00
Andrew Gunnerson a14cab71e5 CHANGELOG.md: Add entry for PR #197
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 18:01:17 -04:00
Andrew Gunnerson c2f4297bb7 Merge pull request #197 from chenxiaolong/deps
Update dependencies
2023-10-27 17:59:15 -04:00
Andrew Gunnerson 9d63c9759d Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 17:44:25 -04:00
Andrew Gunnerson 73b893f3ed changelog.txt: Add entry for PR #196
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 17:42:00 -04:00
Andrew Gunnerson fd7bbf5751 Merge pull request #196 from chenxiaolong/legacy
ota: Add support for legacy OTA metadata
2023-10-27 17:36:03 -04:00
Andrew Gunnerson 409867a8e5 ota: Add support for legacy OTA metadata
Android 11 OTAs use the same `payload.bin` format, but lack the
`metadata.pb` protobuf representation of the OTA metadata. This commit
adds support for parsing the legacy plain-text `metadata` format. Like
before, the output files will still contain both the legacy and
protobuf representations.

Note that the legacy format allowed OEMs to specify arbitrary key/value
pairs. These will be discarded during patching because they cannot be
represented in the protobuf format, which is used in avbroot's internal
representation.

Issue: #195

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 15:15:13 -04:00
Andrew Gunnerson f96a2887df README.md: Add instructions for reverting to stock firmware
Fixes: #194

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-17 15:47:27 -04:00
Andrew Gunnerson 8140e25620 Merge pull request #193 from pascallj/patch-1
Update README.md to use new options
2023-10-15 15:55:52 -04:00
Pascal Roeleven ce05477ab1 Update README.md to use new options
During the migration to avbroot 2.0, these options have been renamed. Reflect this in the Readme.

Signed-off-by: Pascal Roeleven <dev@pascalroeleven.nl>
2023-10-15 21:36:35 +02:00
Andrew Gunnerson 1c4800c0cb CHANGELOG.md: Add entry for PR #191
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-14 17:05:53 -04:00
Andrew Gunnerson d402fdf5f1 Merge pull request #191 from chenxiaolong/sign
boot: Avoid setting signature algorithm when image is indirectly signed
2023-10-14 17:04:29 -04:00
Andrew Gunnerson 4930527598 boot: Avoid setting signature algorithm when image is indirectly signed
Previously, the AVB `algorithm_type` field was unconditionally being set
to a value that is compatible with the AVB private key. However, for
indirectly-signed boot images, the value should be set to `None`. Pixel
bootloaders accept the incorrect value, but other devices' bootloaders
might not.

Issue: #186

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 18:46:52 -04:00
Andrew Gunnerson 9b970c6d25 CHANGELOG.md: Add entry for PR #190
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 18:04:44 -04:00
Andrew Gunnerson e7fb4004cd Merge pull request #190 from chenxiaolong/sha1
avb: Allow computing insecure SHA1 hashes for verification
2023-10-13 18:03:04 -04:00
Andrew Gunnerson 2773dcddcf avb: Allow computing insecure SHA1 hashes for verification
Some partitions, like system_ext in the Pixel Experience build for
`avicii`, use SHA1 for the dm-verity hash tree. This is technically
valid, so allow the `avb verify` command to compute these hashes. SHA1
will still be rejected when creating AVB images.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 17:33:45 -04:00
Andrew Gunnerson 36765290fd CHANGELOG.md: Add entry for PR #189
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 17:32:33 -04:00
Andrew Gunnerson 6d8268e472 Merge pull request #189 from chenxiaolong/reopen
Move reopen functionality to a new trait
2023-10-13 17:31:15 -04:00
Andrew Gunnerson 2e8e86766b Move reopen functionality to a new trait
This is still not the ideal API, but it makes the code quite a bit more
readable since we no longer have to pass around closures everywhere that
multithreaded reads and writes to the same file are needed.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-11 22:09:45 -04:00
Andrew Gunnerson 8ae1c54c13 CHANGELOG.md: Add entry for PR #188
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-11 20:47:03 -04:00
Andrew Gunnerson 5212d4de51 Merge pull request #188 from chenxiaolong/avb_check
Add more checks to ensure AVB is actually providing security
2023-10-11 20:43:07 -04:00
Andrew Gunnerson 61bb9d6a81 Add more checks to ensure AVB is actually providing security
avbroot currently has a check to ensure that AVB signature verification
isn't completely disabled by non-zero vbmeta header flags. This is done
to ensure that the user isn't given a false sense of security when the
OS is built in an insecure way. This commit makes a few changes better
reject insecure OTAs.

The first change is extending the non-zero flags check to all vbmeta
images. Previously, only vbmeta images that needed to be updated as a
result of patching/re-signing were checked.

The second change is ensuring that there are no critical partitions
present in `payload.bin`, but missing from the vbmeta descriptors. For
example, if the vbmeta descriptor for `system.img` is missing, the
patching process will fail with a fatal error that cannot be bypassed.

Unfortunately, it's not feasible to check every partition because there
is no known device where every partition is protected by AVB. Google's
Tensor-based devices are the best and only have a single partition not
protected by AVB: `modem`. Qualcomm-based devices have many partitions
not protected by AVB. OnePlus devices have many partitions that are
checked directly (against the appended header) and thus, aren't listed
in any vbmeta image.

Due to this, only partitions that AOSP understands are checked. OEM-
specific partitions (which, security-wise, may be just as important) are
not checked.

The third change is ensuring that there's only a single root of trust
among the vbmeta images. This way, it's impossible to have, for example,
a `vbmeta_unused` image that contains all the descriptors and an empty
`vbmeta` image that's actually read by the bootloader.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-11 20:28:32 -04:00
Andrew Gunnerson 6d86fbf8a0 CHANGELOG.md: Add entry for PR #184
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-08 20:56:35 -04:00
Andrew Gunnerson 621ccc254c Merge pull request #184 from chenxiaolong/patch
Group `ota patch --help` options into sections
2023-10-08 20:55:19 -04:00
Andrew Gunnerson f41ef844e0 Group ota patch --help options into sections
There are a ton of options and simply displaying them in a giant list is
not very readable.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-07 17:04:38 -04:00
Andrew Gunnerson 42f8769e48 CHANGELOG.md: Add entry for PR #183
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-07 16:38:46 -04:00
Andrew Gunnerson d2187676d1 Merge pull request #183 from chenxiaolong/avb
avb.rs: Fix missing help text for --ignore-invalid
2023-10-07 16:36:52 -04:00
Andrew Gunnerson 3de5194c46 avb.rs: Fix missing help text for --ignore-invalid
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-06 21:05:52 -04:00
Andrew Gunnerson e0c1d4ad3a CHANGELOG.md: Add entry for PR #182
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-04 17:53:39 -04:00
Andrew Gunnerson c94af49009 Merge pull request #182 from chenxiaolong/payload
payload: Bump maximum manifest size to 4 MiB
2023-10-04 17:51:51 -04:00
Andrew Gunnerson 5ed7dd7dca payload: Bump maximum manifest size to 4 MiB
The Android 13 -> 14 incremental OTA has a 1.7 MiB manifest.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-04 15:49:20 -04:00
Andrew Gunnerson dac4e8d328 Version 2.2.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-04 14:58:01 -04:00
Andrew Gunnerson 3cc078ccfb CHANGELOG.md: Add entry for PR #181
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-03 21:03:32 -04:00
Andrew Gunnerson b47b51114f Merge pull request #181 from chenxiaolong/deps
Update ring to 0.17.0
2023-10-03 21:02:26 -04:00
Andrew Gunnerson 09048a00d2 Update ring to 0.17.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-03 20:51:16 -04:00
Andrew Gunnerson ca0e80521f README.md: Fix typo in magisk-info subcommand
Fixes: #180

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-03 14:02:57 -04:00
Andrew Gunnerson 6491106df0 Add names to all Github Actions workflows
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 19:33:58 -04:00
Andrew Gunnerson 2e8fa31207 Merge pull request #179 from chenxiaolong/ci
ci.yml: Only cancel previous runs for pull requests
2023-10-01 19:24:48 -04:00
Andrew Gunnerson d69fdaadae ci.yml: Only cancel previous runs for pull requests
Fixes: #177

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 19:09:58 -04:00
Andrew Gunnerson f0a631951a CHANGELOG.md: Add entry for PR #178
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 16:39:13 -04:00
Andrew Gunnerson 83265e584a Merge pull request #178 from chenxiaolong/cpio
cpio: Only reassign inodes when missing
2023-10-01 16:38:13 -04:00
Andrew Gunnerson ddb8911efd cpio: Only reassign inodes when missing
This way, archives with hard links can pass through `cpio unpack` and
`cpio pack`, even though there's no explicit support for hard links.

This also changes the trailer entry logic to not set an inode number.
AOSP's mkbootfs and magiskboot both start at 300000 and increment by one
for each entry, including the trailer. However, GNU cpio, bsdcpio,
busybox, and toybox all set it to 0, which makes more sense given that
it doesn't represent anything on disk.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 16:16:04 -04:00
Andrew Gunnerson 72be84b2fc CHANGELOG.md: Add entry for PR #176
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 23:19:53 -04:00
Andrew Gunnerson 81d77d8610 Merge pull request #176 from chenxiaolong/prost
Switch to prost for protobuf encoding/decoding
2023-09-30 23:18:55 -04:00
Andrew Gunnerson 4651b755ba Switch to prost for protobuf encoding/decoding
There are several things we had to work around with quick-protobuf, like
forcing no_std mode to use BTreeMaps and avoiding helper functions to
read and write non-size-delimited messages. In addition, the pb-rs code
generator doesn't support adding #[derive]s to enums and the existing
support for adding derives to structs is broken due to incorrect string
concatenation.

Prost doesn't have these limitations and bugs. I originally avoided it
because prost_build required the external `protoc` binary, but now that
the protox library exists, the code generation can be done entirely in
Rust without external tools.

Prost also fully supports adding custom attributes to structs, enums,
and fields. This will be helpful for future payload unpack and pack
commands where the payload manifest would have to be serialized to TOML.

The e2e checksums had to be updated because prost's serialized bytes on
the wire differ from quick-protobuf, despite having the same semantic
meaning. Since all checksums need to be updated anyway, the Magisk apk
and OTA images have all been updated to the latest versions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 22:27:46 -04:00
Andrew Gunnerson 979699e3dc CHANGELOG.md: Add entry for PR #175
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 20:03:06 -04:00
Andrew Gunnerson beee2c7c90 Merge pull request #175 from chenxiaolong/boot
boot: Rename header.toml to boot.toml for consistency
2023-09-30 20:00:11 -04:00
Andrew Gunnerson 206916e544 boot: Rename header.toml to boot.toml for consistency
This also changes the serialization to use a `type` field instead of
spewing the enum variant name everywhere.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 19:43:04 -04:00
Andrew Gunnerson aaed44ad73 CHANGELOG.md: Add entry for PR #174
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 19:42:47 -04:00
Andrew Gunnerson 37b33f803a Merge pull request #174 from chenxiaolong/deps
Update dependencies
2023-09-30 19:41:30 -04:00
Andrew Gunnerson e72b55dc57 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 19:23:30 -04:00
Andrew Gunnerson 5b01feb344 CHANGELOG.md: Add entry for PR #173
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 17:51:18 -04:00
Andrew Gunnerson 2f7556b530 Merge pull request #173 from chenxiaolong/cpio
Add CLI for packing and unpacking cpio archive
2023-09-30 17:49:54 -04:00
Andrew Gunnerson f25196f0a2 Add CLI for packing and unpacking cpio archive
The new commands behave exactly like the existing `avb` and `boot`
subcommands. This completes the last piece of the puzzle for exposing
useful interfaces to avbroot's internal parsers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 00:50:30 -04:00
Andrew Gunnerson 2735a6558e CHANGELOG.md: Add entry for PR #172
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-29 19:10:39 -04:00
Andrew Gunnerson 0c03fceb96 Merge pull request #172 from chenxiaolong/cpio
Add streaming CPIO reader and writer
2023-09-29 19:09:20 -04:00
Andrew Gunnerson 164274eb97 Add streaming CPIO reader and writer
The Magisk boot image patcher still reads all the entries into memory
for simplicity, but everything else now uses the streaming reader.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-29 18:47:37 -04:00
Andrew Gunnerson 61129e8ec7 Version 2.1.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 21:53:15 -04:00
Andrew Gunnerson e1f4fa1ee8 CHANGELOG.md: Add entry for PR #171
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 21:51:15 -04:00
Andrew Gunnerson d15a29e9a7 Merge pull request #171 from chenxiaolong/openat
Use handle-based directory operations instead of path-based
2023-09-28 21:37:44 -04:00
Andrew Gunnerson 26a3bafc3d Use handle-based directory operations instead of path-based
This prevents avbroot writing to locations where it shouldn't due to
manipulation from external processes. For example, replacing the output
directory for `avbroot ota extract` with a symlink.

Regardless of what symlinks or other changes an external process does to
the directory, the cap-std library will use the appropriate `openat2()`
options (or equivalent) to prevent writes outside of the directory.

Fixes: #166

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 21:26:35 -04:00
Andrew Gunnerson 999e31cdf3 CHANGELOG.md: Add entry for PR #170
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:53:01 -04:00
Andrew Gunnerson ae2ebbf2aa Merge pull request #170 from chenxiaolong/payload
payload: Add size limit for protobuf manifest
2023-09-28 19:52:21 -04:00
Andrew Gunnerson fea860a382 payload: Add size limit for protobuf manifest
This is the only part of the payload that we have to read entirely into
memory. The limit is currently set to 1 MiB, which is around ~5-6x the
manifest size in all the e2e test suite's images.

Issue: #169

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:38:38 -04:00
Andrew Gunnerson 98339aee13 CHANGELOG.md: Add entry for PR #169
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:38:19 -04:00
Andrew Gunnerson f8772ef327 Merge pull request #169 from chenxiaolong/ota
ota: Don't preallocate buffer when reading OTA metadata
2023-09-28 19:33:47 -04:00
Andrew Gunnerson 7d62ddc64e ota: Don't preallocate buffer when reading OTA metadata
This way we can't run out of memory if the zip entry lists an invalid
file size that's much bigger than the actual file.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:22:17 -04:00
Andrew Gunnerson d93378cc24 CHANGELOG.md: Add entry for PR #168
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:19:14 -04:00
Andrew Gunnerson 4885e978ee Merge pull request #168 from chenxiaolong/fec
fec: Ensure FEC data size and grid size won't overflow
2023-09-28 19:18:33 -04:00
Andrew Gunnerson ecb2caf47d fec: Ensure FEC data size and grid size won't overflow
This also limits the block size to 16384. There are currently no known
images that use anything larger than 4096.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:07:04 -04:00
Andrew Gunnerson 525e2a409e CHANGELOG.md: Add entry for PR #167
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 18:55:07 -04:00
Andrew Gunnerson 7e89e9081a Merge pull request #167 from chenxiaolong/fuzz
Add fuzzer for FEC image parser
2023-09-28 18:54:25 -04:00
Andrew Gunnerson 2a57343094 Add fuzzer for FEC image parser
Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 18:41:47 -04:00
Andrew Gunnerson cd779e62cb CHANGELOG.md: Add entry for PR #165
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 23:50:08 -04:00
Andrew Gunnerson 96fb8ddc9c Merge pull request #165 from chenxiaolong/cpio
Add fuzzer for cpio
2023-09-27 23:49:23 -04:00
Andrew Gunnerson 0aba185c85 Add fuzzer for cpio
Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 23:21:04 -04:00
Andrew Gunnerson 36dcbe092f CHANGELOG.md: Add entry for PR #164
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 22:22:34 -04:00
Andrew Gunnerson aab9f5216f Merge pull request #164 from chenxiaolong/cpio
cpio: Read data by incremental reallocation after a threshold
2023-09-27 22:21:55 -04:00
Andrew Gunnerson 2a2515c5ef cpio: Read data by incremental reallocation after a threshold
This prevents avbroot from being killed for excessive memory usage due
to the entry header specifying a very large file name or file size
(larger than what's actually contained in the file).

This threshold for switching to incremental reallocation is 1 KiB for
file names and 1 MiB for file data.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 21:21:29 -04:00
Andrew Gunnerson 111ec6ff1e CHANGELOG.md: Add entry for PR #133
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 20:55:48 -04:00
Andrew Gunnerson cfd1dfb2eb Merge pull request #133 from chenxiaolong/deny
cargo-deny: Block executables in dependencies
2023-09-27 20:54:45 -04:00
Andrew Gunnerson d7977bab93 cargo-deny: Block executables in dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 20:44:22 -04:00
Andrew Gunnerson c29d78181a CHANGELOG.md: Add entry for PR #163
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 20:36:59 -04:00
Andrew Gunnerson 42a9a9a12b Merge pull request #163 from chenxiaolong/fuzz
avb: Use checked addition when computing auth/aux block size
2023-09-27 20:35:54 -04:00
Andrew Gunnerson e293331952 avb: Use checked addition when computing auth/aux block size
(Found by honggfuzz)

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:43:15 -04:00
Andrew Gunnerson bf97444f4b CHANGELOG.md: Add entry for PR #162
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:42:48 -04:00
Andrew Gunnerson 1fa068b465 Merge pull request #162 from chenxiaolong/fuzz
bootimage: Fix potential divide-by-0 and multiplication overflow
2023-09-27 19:41:53 -04:00
Andrew Gunnerson f85b6eec46 bootimage: Fix panic when validating vendor v4 table size
The multiplication can overflow.

(Found by honggfuzz)

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:28:53 -04:00
Andrew Gunnerson 07f2e30430 bootimage: Ensure page_size is never 0
Otherwise, we'll panic due to dividing by 0.

(Found by honggfuzz)

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:28:52 -04:00
Andrew Gunnerson 9f34198f07 CHANGELOG.md: Add entry for PR #161
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:28:23 -04:00
Andrew Gunnerson 9d8e82651e Merge pull request #161 from chenxiaolong/honggfuzz
Add initial honggfuzz fuzzing infrastructure
2023-09-27 19:27:00 -04:00
Andrew Gunnerson 5b22d7b603 Add initial honggfuzz fuzzing infrastructure
This initially includes fuzzers for the AVB and boot image parsers. The
initial input corpus are the same test files we use for the round trip
tests.

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:15:42 -04:00
Andrew Gunnerson 11d19de0a1 CHANGELOG.md: Add entry for PR #158, #159
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-26 23:21:44 -04:00
Andrew Gunnerson fc4c2e63da Merge pull request #159 from chenxiaolong/limits
bootimage: Enforce size limits to prevent DoS
2023-09-26 23:17:40 -04:00
Andrew Gunnerson ee28a57a74 bootimage: Enforce size limits to prevent DoS
The kernel, ramdisk(s), second stage bootloader, and device tree
components are limited to 64 MiB. In practice, no device has boot
partitions larger than 64 MiB, so this limit should never be hit. The
vendor v4 bootconfig component is limited to 1 KiB, which is about ~25x
the size of the Pixel 7 Pro's bootconfig.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-26 23:02:24 -04:00
Andrew Gunnerson 101512b894 Merge pull request #158 from chenxiaolong/limits
avb: Enforce size limits to prevent DoS
2023-09-26 20:52:18 -04:00
Andrew Gunnerson 50913db375 avb: Enforce size limits to prevent DoS
AVB 2.0 mandates that the header is 64 KiB or less, so enforce this both
when parsing and when writing vbmeta headers. Hash trees and FEC data
are also read into memory, so enforce limits corresponding to a 4 GiB
partition image.

Since these limits are so small, this lets us remove nearly all checked
integer conversions since a direct cast will never result in truncation.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-26 20:38:19 -04:00
Andrew Gunnerson 699ea92dbf CHANGELOG.md: Add entry for PR #156
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-21 01:47:08 -04:00
Andrew Gunnerson 667b99254c Merge pull request #156 from chenxiaolong/passphrase
Consolidate logic for determining passphrase source priority
2023-09-21 01:45:46 -04:00
Andrew Gunnerson e41814a425 Consolidate logic for determining passphrase source priority
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-20 23:35:25 -04:00
Andrew Gunnerson dff2fff028 Version 2.1.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 23:56:58 -04:00
Andrew Gunnerson 67bca2e58c CHANGELOG.md: Add entry for PR #153
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 23:54:51 -04:00
Andrew Gunnerson 1f08cb6d40 Merge pull request #154 from chenxiaolong/changelog
Shorten changelog links
2023-09-19 23:53:28 -04:00
Andrew Gunnerson ed68f09cb9 Shorten changelog links
The longer PR links have weird line wrapping behavior on the Github web
UI, making it more difficult to read.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 23:09:23 -04:00
Andrew Gunnerson 64c11c2dc3 Merge pull request #153 from chenxiaolong/error
Disambiguate some error fields and add more error context
2023-09-19 23:08:15 -04:00
Andrew Gunnerson 5115134b5a Disambiguate some error fields and add more error context
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 22:54:43 -04:00
Andrew Gunnerson a2d81d6bfe CHANGELOG.md: Add entry for PR #152
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 22:53:14 -04:00
Andrew Gunnerson c5709b90f6 Merge pull request #152 from chenxiaolong/avb
Add CLI for packing and unpacking AVB images
2023-09-19 22:49:07 -04:00
Andrew Gunnerson 2928b96f1f Add CLI for packing and unpacking AVB images
This supports all of the `format::avb` functionality, including
repairing dm-verity images. When packing images, all offsets, sizes,
digests, hash trees, FEC data, signatures, etc. are automatically
recomputed. The goal is that the user can edit any partition image
without needing to think about AVB at all.

A new `--repair` option has also been added to the `avb verify` command
to automatically attempt to all dm-verity images.

As a side effect of the changes, RSA2048 keys are now supported.

This commit also removes the `Clone` implementation from the `PSeekFile`
and `SharedCursor` types. These types use the same underlying file or
memory buffer when cloned, allowing parallel threads to read and write
files using the normal `Read`/`Write` APIs. The intention is that
cloning one of these instances would behave as if a new file handle to
the same file was opened. However, the file offset was also copied
instead of being set to 0, which is confusing. A new `reopen()` method
has been added that explicitly sets the initial offset to 0.

Closes: #148

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 22:16:03 -04:00
Andrew Gunnerson 5638a8bb4d CHANGELOG.md: Add entry for PR #150
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 21:27:06 -04:00
Andrew Gunnerson a582d0b3e6 Merge pull request #150 from chenxiaolong/escape
Switch to bstr for escaping mostly UTF-8 binary data
2023-09-18 21:25:49 -04:00
Andrew Gunnerson 1d08ffa527 Switch to bstr for escaping mostly UTF-8 binary data
bstr is Unicode-aware and won't split multi-byte codepoints into
multiple `\x##`.

This commit also adds an `escape` module for use with serde so we can
serialize mostly UTF-8 binary data into human-readable files, like TOML.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 21:10:47 -04:00
Andrew Gunnerson a95ce56246 CHANGELOG.md: Add entry for PR #149
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 17:18:26 -04:00
Andrew Gunnerson ca6ce1fb37 Merge pull request #149 from chenxiaolong/stderr
Print status and warning messages to stderr
2023-09-18 17:16:56 -04:00
Andrew Gunnerson d7a46dcaaf Print status and warning messages to stderr
Also, fix warning!() to allow 0 arguments to match status!() and
println!().

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 16:48:25 -04:00
Andrew Gunnerson a4978aaac0 CHANGELOG.md: Add entry for PR #146
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 16:39:13 -04:00
Andrew Gunnerson 6ca4023b0d Merge pull request #146 from chenxiaolong/fec
Add support for AVB FEC
2023-09-18 16:37:17 -04:00
Andrew Gunnerson acc3308d37 Hash tree is two words
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 15:58:38 -04:00
Andrew Gunnerson 9ca3a690df Verify FEC data in HashtreeDescriptor::verify()
This also fixes the wrong error being reported when the hash tree does
not match and adds additional checks to ensure that there are no gaps
between the image data, hash tree, and FEC data.

Issue: #145

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 15:57:40 -04:00
Andrew Gunnerson 335c04711e Add support for AVB FEC
This commit adds initial support for the FEC (forward error correction)
used by dm-verity. The new `avbroot fec` commands operate on FEC data
stored in AOSP's standalone FEC file format. It is compatible with
AOSP's `fec` tool.

Issue: #145

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 15:57:09 -04:00
Andrew Gunnerson fb7be77480 CHANGELOG.md: Add entry for PR #147
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 00:27:56 -04:00
Andrew Gunnerson e683fce4ab Merge pull request #147 from chenxiaolong/atomicbool
Stopping spewing Arc everywhere for the cancel signal
2023-09-18 00:24:29 -04:00
Andrew Gunnerson 7493cdc5fa Stopping spewing Arc everywhere for the cancel signal
There's no reason all these functions need to care about the ownership
of the cancel signal.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 00:12:09 -04:00
Andrew Gunnerson 8f49f49154 Merge pull request #143 from AgentOak/additional-readme-notes
README.md: Add additional notes about lost keys and OTA updaters
2023-09-12 16:22:19 -04:00
Jan Erik Petersen bc6fdd8513 README.md: Add additional notes about lost keys and OTA updaters 2023-09-12 21:09:26 +02:00
Andrew Gunnerson f6c86473c3 Merge pull request #141 from chenxiaolong/sync
e2e: Switch to attohttpc for HTTP downloads
2023-09-11 18:40:32 -04:00
Andrew Gunnerson d0d51f562c e2e: Switch to attohttpc for HTTP downloads
We don't use async for anything else, so switching to a synchronous HTTP
library lets us get rid of the entire async ecosystem and removes 47
packages from e2e's dependency tree.

The only downside is that ^C worst case takes 5 seconds (TCP connect and
read timeouts) or whatever the OS's DNS lookup timeout is. That's good
enough for a test suite.

Performance wise, e2e still easily saturates a gigabit internet
connection.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-09 23:34:48 -04:00
Andrew Gunnerson f31c4134e4 Version 2.0.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 14:01:00 -04:00
Andrew Gunnerson dd0d7f334b CHANGELOG.md: Add entry for PR #140
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:59:43 -04:00
Andrew Gunnerson e4a2a4c24c Merge pull request #140 from chenxiaolong/version
Add `--version` option
2023-09-07 13:58:50 -04:00
Andrew Gunnerson cc9aab197c CHANGELOG.md: Add entry for PR #139
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:58:23 -04:00
Andrew Gunnerson 92da30f53c Merge pull request #139 from chenxiaolong/xz
Upgrade xz to 5.4.4 and enable all encoders and decoders
2023-09-07 13:52:14 -04:00
Andrew Gunnerson 26142b0271 Upgrade xz to 5.4.4 and enable all encoders and decoders
When using the xz2/static feature, the xz2 crate uses a bundled version
of xz 5.2 and doesn't enable all of the available encoders and decoders.
This prevents certain payload data from being decompressed.

Fixes: #138

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:40:34 -04:00
Andrew Gunnerson c8df3286df Add --version option
Issue: #138

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:37:24 -04:00
Andrew Gunnerson 65979beff4 Version 2.0.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 23:16:01 -04:00
Andrew Gunnerson 47cf015f15 CHANGELOG.md: Add entry for PR #137
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 23:15:19 -04:00
Andrew Gunnerson e40e036159 Merge pull request #137 from chenxiaolong/anyhow
Remove unnecessary use of anyhow macro
2023-09-06 23:13:04 -04:00
Andrew Gunnerson 0d2a158ead Remove unnecessary use of anyhow macro
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 23:04:18 -04:00
Andrew Gunnerson ddabb19b4a CHANGELOG.md: Add entry for PR #136
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 22:51:58 -04:00
Andrew Gunnerson 3c05eb300c Merge pull request #136 from chenxiaolong/offset
PayloadWriter: Only set data_offset for operations that reference blobs
2023-09-06 22:50:31 -04:00
Andrew Gunnerson 2ffb1dfdbd PayloadWriter: Only set data_offset for operations that reference blobs
This fixes `data_offset` being set for `ZERO` and `DISCARD` operations,
which prevents some images (eg. `ossi`) from being flashed due to
update_engine's strict field validation.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 22:40:15 -04:00
Andrew Gunnerson f93dc55f70 CHANGELOG.md: Add entry for PR #135
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 20:05:41 -04:00
Andrew Gunnerson 393e65f6b4 Merge pull request #135 from chenxiaolong/full
Move full OTA check to patch/extract subcommand function
2023-09-06 20:03:47 -04:00
Andrew Gunnerson 3f86a67038 Move full OTA check to patch/extract subcommand function
This way, the payload-related functions can be used to parse incremental
OTA files.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 19:51:48 -04:00
Andrew Gunnerson 4337170f57 Version 2.0.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-04 17:11:28 -04:00
Andrew Gunnerson a73365592f CHANGELOG.md: Add entry for PR #132
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-04 16:41:28 -04:00
Andrew Gunnerson 0018e8a5ba Merge pull request #132 from chenxiaolong/magisk
boot.rs: Allow Magisk 263xx
2023-09-04 16:39:14 -04:00
Andrew Gunnerson adf0014da2 boot.rs: Allow Magisk 263xx
There are no changes that break avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-04 16:30:39 -04:00
Andrew Gunnerson 1179f9f2fa Version 2.0.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-31 23:43:22 -04:00
Andrew Gunnerson bbe793bd2d Merge pull request #130 from chenxiaolong/rust
avbroot 2.0: Rewrite in Rust
2023-08-31 23:42:34 -04:00
Andrew Gunnerson d643b51579 xtask: Update changelog version in set-version
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-31 20:51:13 -04:00
Andrew Gunnerson a25d9a9eb4 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-31 20:49:31 -04:00
Andrew Gunnerson b92513609b Move main avbroot code to a workspace member
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-31 20:49:29 -04:00
Andrew Gunnerson 44d62f5147 Add release management tasks
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-30 19:23:07 -04:00
Andrew Gunnerson fb4b93b403 ota verify: Add check for the ramdisk's otacerts.zip
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-29 19:25:27 -04:00
Andrew Gunnerson c6ac508a50 Work around upstream bzip2 infinite loop issue
There's an upstream bug that causes an infinite loop in the
`write::BzDecoder` destructor if the decoder is fed invalid data. While
this never happens during normal operation, it is possible to run into
this by running `ota extract` against a `--stripped` OTA file.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-29 19:23:50 -04:00
Andrew Gunnerson 6d892d37ff Add support for Magisk v26.2
There is nothing new that requires changes on the avbroot side.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-29 16:25:05 -04:00
Andrew Gunnerson 8549fa1dfc avbroot 2.0: Rewrite in Rust
Why?
----

It was always my intention to write avbroot in a compiled language.
Python was a stop-gap solution since it was possible to use the various
tools and parsers from AOSP to make the initial prototyping and
implementation easier. However, doing so required a whole lot of hacks
since nearly all of the Python modules we use were intended to be used
as executables, not libraries, and they were definitely not meant to be
used outside of AOSP's code base.

Although the dependencies on AOSP code have been reduced over time,
working on the Python code is still frustrating. The majority of the
modules we use from both the standard library and external dependencies
are lacking type annotations. All of the Python language servers and
type checker tools I've used choked on them. There have been serveral
avbroot bugs in the past that wouldn't have happened with any
statically typed language.

The catalyst for me working on this recently was dealing with some
python-protobuf versions that wouldn't work with AOSP's pregenerated
protobuf bindings. When parsing protobuf messages, it would fail
with obscure runtime type errors. I need my projects to not feel
frustrating or else I'll just get burnt out.

Hence, the Rust rewrite. With fewer hacks this time! avbroot no longer
has any dependencies on external tools like openssl. I'll be providing
precompiled binaries for the three major desktop OS's, built by GitHub
Actions. avbroot will also be versioned now, starting at 2.0.0.

Whats new?
----------

* A new `avbroot ota verify` subcommand has been added to check that all
  OTA and AVB related components have been properly hashed and signed.
  This works for all OTA images, including stock ones.
* A couple new `avbroot avb` subcommands have been added for dumping
  vbmeta header/footer information and verifying AVB signatures. These
  are roughly equivalent to avbtool's `info_image` and `verify_image`
  subcommands, though avbroot is about an order of magnitude faster than
  the latter.
* A new set of `avbroot boot` subcommands have been added for packing
  and unpacking boot images. It supports Android v0-v4 images and vendor
  v3-v4 images. Repacking is lossless even when using deprecated fields,
  like the boot image v4 VTS signature.
* A new `avbroot ramdisk` subcommand has been added for inspecting
  the CPIO structure of ramdisks.
* A new set of `avbroot key` subcommands have been added for generating
  signing keys so that it's no longer necessary to install openssl and
  avbtool (though of course, keys generated by other tools remain fully
  compatible).
* Since avbroot has a ton of CLI options, a new `avbroot completion`
  subcommand has been added for generating tab-completion configs for
  various shells (eg. bash, zsh, fish, powershell).

What was removed?
-----------------

Nothing :) The `patch` and `extract` subcommands have been moved under
`avbroot ota` and the `magisk-info` subcommand has been moved under
`avbroot boot`, but there are compatibility shims in place to keep all
the old commands working.

The command-line interface will remain backwards compatible for as long
as possible, even with new major releases. The Rust API, however, has no
backwards compatibility guarantees. I currently don't intend for
avbroot's "library" components to be used anywhere outside of Custota
and avbroot itself.

Performance
-----------

Due to having better access to low-level APIs (especially `pread` and
`pwrite`), nearly everything that can be multithreaded in avbroot is now
multithreaded. In addition, during the patching operation, everything
is done entirely in memory without temp files and the maximum memory
usage is still about 100MB lower than with the Python implementation.

The new implementation is bottlenecked by how fast a single CPU core can
calculate 3 SHA256 hashes of overlapping regions spanning the majority
of the OTA file. About 90% of the CPU time is spent calculating SHA256
hashes and another 5% or so performing XZ-compression.

Some numbers:

* Patching should take roughly 40%-70% of the time it took before.
* Extracting with `--all` should take roughly 10%-30% of the time it
  took before.

Folks with x86_64 CPUs supporting SHA-NI extensions (eg. Intel 11th gen
and newer) should see even bigger improvements.

Reproducibility
---------------

The new implementation's output files are bit-for-bit identical when the
inputs are the same. However, they do not exactly match what the Python
implementation produced.

* The zip entries, aside from `metadata` and `metadata.pb`, are written
  in sorted order.
* All zip entries are stored without compression.
* All zip entries are stored without additional metadata (eg.
  modification timestamp).
* The OTA certificate, both in the OTA zip and in the recovery ramdisk's
  `otacerts.zip`, goes through deserialization + serialization before
  being written. Text in the certificate file before the header and
  after the footer will be stripped out.
* The protobuf structures (payload header and OTA metadata) are
  serialized differently. Protobuf has more than one way to encode the
  same messages "on the wire". The Rust quick_protobuf library
  serializes messages a bit differently than python-protobuf, but the
  outputs are mutually compatible.
* XZ compression of modified partition images in the payload is now done
  at compression level 0 instead of 6. This reduces the patching time by
  several seconds at the cost of a couple MiB increase in file size.
* Ramdisks are now compressed with standard LZ4 instead of LZ4HC (high
  compression mode). For our use case, the difference is <100 KiB, but
  using standard LZ4 allows us to use a pure-Rust LZ4 library and makes
  the compression step much faster.
* Older ramdisks compressed with gzip are slightly different due to a
  different gzip implementation being used (flate2 vs. zlib). The two
  implementations structure the gzip frames slightly differently, but
  the output is identical when decompressed.
* Magisk's config file in the ramdisk (`.backup/.magisk`) will have the
  `SHA1` field set to all zeros. This allows avbroot to keep track of
  less information during patching for better performance. The field is
  only used for Magisk's uninstall feature, which can't ever be used in
  a locked bootloader setup anyway.

Misc
----

While working on the new `avbroot ota verify` subcommand, I found that
the `ossi` stock image (OnePlus 10 Pro) used in avbroot's tests has an
invalid vbmeta hash for the `odm` partition. I thought it was an avbroot
bug, but AOSP's avbtool reports the same invalid hash too. If that image
actually boots, then I'm not sure AVB can be trusted on those devices...

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-08-29 15:54:53 -04:00
156 changed files with 33493 additions and 6798 deletions
+5
View File
@@ -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] }}
+144 -157
View File
@@ -5,173 +5,160 @@ 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 }}
ANDROID_API: ${{ matrix.artifact.android_api }}
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
- os: ubuntu-latest
name: aarch64-linux-android31
targets:
- aarch64-linux-android
android_api: '31'
steps:
- uses: actions/checkout@v3
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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: Install qemu-user-static
if: ${{ contains(matrix.artifact.name, 'android') }}
shell: bash
run: |
import json
import os
import sys
sudo apt-get -y update
sudo apt-get -y install qemu-user-static
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: Set Android temporary directory
if: ${{ contains(matrix.artifact.name, 'android') }}
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
echo "TMPDIR=/tmp" >> "${GITHUB_ENV}"
- name: Checking for cached tox environments
id: get-tox-cache
uses: actions/cache/restore@v3
- name: Install cargo-android
shell: bash
run: |
cargo install \
--git https://github.com/chenxiaolong/cargo-android \
--tag v0.1.3
- name: Get version
id: get_version
shell: bash
run: |
echo -n 'version=' >> "${GITHUB_OUTPUT}"
git describe --always \
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
>> "${GITHUB_OUTPUT}"
- name: Install toolchains
shell: bash
run: |
for target in ${TARGETS}; do
rustup target add "${target}"
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
key: ${{ steps.cache-keys.outputs.tox-key-prefix }}
lookup-only: true
key: ${{ matrix.artifact.name }}
- name: Clippy
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
clippy --release --workspace --features static \
--target "${target}"
done
- name: Build
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
build --release --workspace --features static \
--target "${target}"
done
- name: Tests
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
test --release --workspace --features static \
--target "${target}"
done
- name: End to end tests
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
run --release -p e2e --features static \
--target "${target}" \
-- test -a -c e2e/e2e.toml
done
- name: Create output directory
shell: bash
run: |
rm -rf target/output
case "${{ matrix.artifact.combine }}" in
lipo)
mkdir target/output
cmd=(lipo -output target/output/avbroot -create)
for target in ${TARGETS}; do
cmd+=("target/${target}/release/avbroot")
done
"${cmd[@]}"
;;
'')
ln -s "${TARGETS}/release" target/output
;;
*)
echo >&2 "Unsupported combine argument"
exit 1
;;
esac
# This is done to ensure a flat directory structure. The upload-artifact
# action no longer allows multiple uploads to the same destination.
- name: Copy documentation to target directory
shell: bash
run: cp LICENSE README.md target/output/
- name: Archive executable
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
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
+16
View File
@@ -0,0 +1,16 @@
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@30f817c6f72275c6d54dc744fbca09ebc958599f # v2.0.12
-29
View File
@@ -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/
+35
View File
@@ -0,0 +1,35 @@
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Create release
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
tag_name: v${{ steps.get_version.outputs.version }}
name: Version ${{ steps.get_version.outputs.version }}
body_path: RELEASE.md
draft: true
+5 -3
View File
@@ -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
-9
View File
@@ -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
+554
View File
@@ -0,0 +1,554 @@
<!--
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.18.0
* Make OTA metadata property file field validation more lenient ([Issue #469], [PR #470])
* Fixes `avbroot ota verify` for stock OTAs that include extra zip file entries in the metadata
* Remove automatic promotion of insecure SHA-1 AVB hash algorithm to SHA-256 ([Issue #366], [Issue #469], [PR #473])
* There are insecure devices that don't support SHA-256 and won't boot with it.
* The original feature was a bandaid for OnePlus devices to make them a tiny bit more secure. They used SHA-256 for every partition except `system`. However, OnePlus no longer supports custom AVB keys anyway, so this feature is going away.
* Add support for Magisk 30200 ([PR #474])
* Update dependencies ([PR #475])
### Version 3.17.2
* Add support for Magisk 30100 ([PR #468])
### Version 3.17.1
* Update end-to-end tests to place streaming and seekable OTAs in separate directories for easier troubleshooting ([PR #463])
* Update dependencies ([PR #464])
* Add support for Magisk 30000 ([PR #467])
### Version 3.17.0
* Fix reserved space error when patching OTA zips larger than ~10 GB ([Issue #451], [PR #452])
* Update dependencies ([PR #453])
### Version 3.16.1
* Add support for Magisk 29000 ([PR #448])
* Update dependencies ([PR #449])
### Version 3.16.0
* Add support for CoW version 3 for virtual A/B ([Issue #441], [PR #442], [PR #445])
* This was recently introduced with the Pixel 9a. Previous devices all used CoW version 2.
* Add support for uncompressed CoW for virtual A/B ([PR #443])
* This is not used on actual devices, but is very useful for testing the CoW estimation logic.
* All differences between avbroot's and AOSP delta_generator's estimation logic are now fixed.
* Add support for custom CoW compression levels for virtual A/B ([PR #444])
* This is also not used on actual devices, but is supported by AOSP, so avbroot should support it too.
* Update dependencies ([PR #446])
### Version 3.15.0
* Add support for changing the virtual A/B compression algorithm ([PR #437])
* For devices that launched with Android <14, `--vabc-algo lz4` can significantly increase OTA installation speed when using a custom OTA updater app (with caveats). There is no difference when sideloading from recovery mode.
* See [the documentation](./README.md#changing-virtual-ab-cow-compression-algorithm) for more details.
* Switch back to the ring library now that it is maintained again ([PR #438])
* Update dependencies ([PR #439])
### Version 3.14.0
* Report as many errors as possible before failing in `avbroot ota verify` and improve error messages ([Discussion #426], [PR #428], [PR #430])
* Fix new clippy warnings introduced in Rust 1.85 ([PR #429])
* Fix massive performance regression introduced in 3.13.0 for OTAs that use gzip for virtual A/B CoW compression ([Issue #433], [PR #434])
* Update dependencies ([PR #435])
### Version 3.13.0
* Fix parsing Samsung `super.img` files in `avbroot lp` due to Samsung putting their own data structures in a region that's supposed to be filled with zeros ([PR #415])
* Add advanced option to skip replacing the OTA certificate in the system image ([Discussion #417], [PR #418])
* Switch to stable bzip2-rs release and use zlib-rs as the backend for flate2 ([PR #421])
* Switch to the aws-lc cryptography library for SHA1 and SHA2 hashing ([PR #422])
* The ring library is no longer maintained
* Fix incorrect `Partitions aren't protected by AVB: system` warning when using `--skip-system-ota-cert` ([PR #423])
* Discard unneeded temp file sooner when using `--skip-system-ota-cert` ([PR #424])
* Make `avbroot lp`'s parser less strict so that it can load on-device `super` partitions ([PR #425])
* The on-disk layout on virtual A/B devices violates some requirements stated in AOSP's documentation
* Update dependencies ([PR #427])
### Version 3.12.0
* Add new `-p <name>` option to `avbroot ota extract` for extracting specific partitions ([PR #408])
* Deprecate the `--boot-only` option in `avbroot ota extract` ([PR #408])
* The option will remain indefinitely for backwards compatibility, but is hidden from `--help`
* Add support for extracting the embedded OTA certificate and AVB public key in `avbroot ota extract` ([PR #409])
* Rename `avbroot key extract-avb` to `avbroot key encode-avb` for consistency with `avbroot key decode-avb` ([PR #410])
* The old syntax will remain supported indefinitely for backwards compatibility, but is hidden from `--help`
* Update dependencies ([PR #411])
### Version 3.11.0
* Fix crash when ignoring warning about `--magisk-preinit-device` not being specified ([PR #394])
* When using `--ignore-magisk-warnings`, assume that unsupported Magisk versions newer than the latest supported version are capable of all features ([Issue #393], [PR #395])
* Update bzip2-rs and switch to the Rust backend ([PR #397], [PR #402])
* Minor code cleanup for custom integer range type ([PR #398])
* Improve errors to make them less ambiguous about what went wrong ([PR #401])
* Fix bug where a vendor v4 boot image that was truncated in the bootconfig padding section would be accepted as valid ([PR #401])
* Avoid performing many small I/O operations when reading and writing cpio archives ([PR #403])
* Update dependencies ([PR #404])
### Version 3.10.0
* Switch to using zerocopy library for all binary file format parsers ([PR #384])
* Update to latest AOSP protobuf schema for the `payload.bin` metadata file format ([PR #385])
* Update dependencies and pin Github Actions actions to specific commits ([PR #386], [PR #392])
* Improve error messages from file format parsers ([PR #390])
* Add support for Magisk 28100 ([PR #391])
### Version 3.9.0
* Update all dependencies ([PR #368], [PR #377])
* Add advanced option to skip replacing the OTA certificate in the recovery image ([Issue #366], [PR #367], [PR #371])
* Improve error message when an incompatible RSA key is used for AVB signing ([Issue #366], [PR #369])
* Fix clippy warnings ([PR #370])
* Allow `avbroot ota verify` to verify OTAs that lack `META-INF/com/android/metadata.pb` ([Issue #366], [PR #373])
* Allow `avbroot ota verify` to verify OTAs where the payload signature does not set `unpadded_signature_size` ([Issue #366], [PR #374])
* Allow `avbroot sparse` to parse sparse images with unknown fields (matches AOSP implementation) ([PR #376])
### Version 3.8.0
* Add `avbroot avb digest` subcommand for computing the special vbmeta digest ([PR #363])
* Update all dependencies ([PR #364])
### Version 3.7.1
* Add support for Magisk 28000 ([PR #362])
### Version 3.7.0
* Fix a nasty regression since version 2.0.0 where recovery mode's `otacerts.zip` modifications were lost when using `--prepatched` with Magisk on some older devices, like the Pixel 4a ([Issue #356], [PR #357])
* This affected older devices without `vendor_boot` or `recovery` partitions.
* **This caused sideloading patched OTA updates from recovery mode to break on the affected devices.** To fix the problem without wiping the device and starting fresh, please follow the [steps in the PR](https://github.com/chenxiaolong/avbroot/pull/357#issuecomment-2365343050).
* Print a useful error message when trying to prompt for a passphrase without an interactive terminal ([PR #336])
* Add a new `--zip-mode seekable` option to allow writing OTA zip files without data descriptors ([Issue #328], [PR #337])
* Add new commands for packing and unpacking logical partition images (`super.img`) ([PR #342], [PR #343])
* Add new commands for packing and unpacking Android sparse images ([PR #347])
* Allow `avbroot payload repack` and `avbroot payload info` commands to read delta payloads ([PR #354])
* Switch to passterm library for password prompts ([PR #355])
### Version 3.6.0
* Add support for gzip compression when computing CoW size estimates ([Issue #332], [PR #333])
* This allows `--replace` to successfully replace dynamic partitions on legacy devices, like the Pixel 4a 5G
* Minor code cleanup ([PR #334], [PR #335])
### Version 3.5.0
* Update all dependencies ([PR #329])
* Add new unpack and pack commands for `payload.bin` files ([Issue #328], [PR #331])
### Version 3.4.1
* Update all dependencies ([PR #321])
* Add support for Magisk 27006 ([PR #323])
### Version 3.4.0
* Fix (unreachable) minor error handling logic when attempting to use unsupported AVB signing algorithms ([PR #311])
* Add support for performing signing operations with external programs ([Issue #310], [PR #312])
* See the linked issue for an example of how to sign with a Yubikey.
### Version 3.3.0
* Recompute CoW size estimate when replacing dynamic partitions ([Issue #306], [PR #307])
* Fixes out of space error when flashing a patched OTA that uses `--replace` to replace a dynamic partition (eg. `system`) with a larger or more incompressible image
* Add `avbroot payload info` subcommand for inspecting `payload.bin` headers ([PR #309])
### Version 3.2.3
* Add prebuilt binary for Android (aarch64) ([PR #304])
### Version 3.2.2
* Add new `--recompute-size` option to `avbroot avb pack` to automatically recompute the image size for resizable images ([Discussion #294], [PR #296])
* Add new `--output-info` option to `avbroot avb pack` to write a new `avb.toml` file containing computed values ([PR #297])
* Add support for upcoming Magisk Canary 27003 ([Issue #301], [PR #268])
### Version 3.2.1
* Increase hash tree and FEC size limits to accommodate partition images up to 8 GiB ([Issue #291], [PR #293])
### Version 3.2.0
* Fix potential infinite loop when interrupting avbroot at the right moment to a bug in the bzip2-rs library ([Issue #285], [PR #287])
* Update all dependencies and fix new clippy lints ([PR #288])
* Add support for adding the custom AVB public key to the list of trusted keys for DSU (booting signed GSIs) ([Discussion #286], [PR #289])
### Version 3.1.3
* Build universal binary for macOS ([Issue #278], [PR #279])
### Version 3.1.2
* Use `fastboot flashall` for initial setup to avoid needing to manually flash every partition ([PR #253])
* Remove binary test files in the git repo and generate them at runtime ([Issue #265], [PR #276])
* Fix portions of a couple error messages being incorrectly quoted ([PR #277])
### Version 3.1.1
* Cache salted SHA-256 contexts for a small performance improvement ([PR #257])
* Fix loading certificates that have extra text outside of the marker lines ([PR #261])
### Version 3.1.0
* The `OEMUnlockOnBoot` module has been split out to a separate repo ([Discussion #235], [PR #246])
* 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
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
[Issue #148]: https://github.com/chenxiaolong/avbroot/issues/148
[Issue #157]: https://github.com/chenxiaolong/avbroot/issues/157
[Issue #160]: https://github.com/chenxiaolong/avbroot/issues/160
[Issue #166]: https://github.com/chenxiaolong/avbroot/issues/166
[Issue #201]: https://github.com/chenxiaolong/avbroot/issues/201
[Issue #203]: https://github.com/chenxiaolong/avbroot/issues/203
[Issue #216]: https://github.com/chenxiaolong/avbroot/issues/216
[Issue #218]: https://github.com/chenxiaolong/avbroot/issues/218
[Issue #222]: https://github.com/chenxiaolong/avbroot/issues/222
[Issue #223]: https://github.com/chenxiaolong/avbroot/issues/223
[Issue #225]: https://github.com/chenxiaolong/avbroot/issues/225
[Issue #265]: https://github.com/chenxiaolong/avbroot/issues/265
[Issue #278]: https://github.com/chenxiaolong/avbroot/issues/278
[Issue #285]: https://github.com/chenxiaolong/avbroot/issues/285
[Issue #291]: https://github.com/chenxiaolong/avbroot/issues/291
[Issue #301]: https://github.com/chenxiaolong/avbroot/issues/301
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
[Issue #310]: https://github.com/chenxiaolong/avbroot/issues/310
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
[Issue #441]: https://github.com/chenxiaolong/avbroot/issues/441
[Issue #451]: https://github.com/chenxiaolong/avbroot/issues/451
[Issue #469]: https://github.com/chenxiaolong/avbroot/issues/469
[PR #130]: https://github.com/chenxiaolong/avbroot/pull/130
[PR #132]: https://github.com/chenxiaolong/avbroot/pull/132
[PR #133]: https://github.com/chenxiaolong/avbroot/pull/133
[PR #135]: https://github.com/chenxiaolong/avbroot/pull/135
[PR #136]: https://github.com/chenxiaolong/avbroot/pull/136
[PR #137]: https://github.com/chenxiaolong/avbroot/pull/137
[PR #139]: https://github.com/chenxiaolong/avbroot/pull/139
[PR #140]: https://github.com/chenxiaolong/avbroot/pull/140
[PR #146]: https://github.com/chenxiaolong/avbroot/pull/146
[PR #147]: https://github.com/chenxiaolong/avbroot/pull/147
[PR #149]: https://github.com/chenxiaolong/avbroot/pull/149
[PR #150]: https://github.com/chenxiaolong/avbroot/pull/150
[PR #152]: https://github.com/chenxiaolong/avbroot/pull/152
[PR #153]: https://github.com/chenxiaolong/avbroot/pull/153
[PR #156]: https://github.com/chenxiaolong/avbroot/pull/156
[PR #158]: https://github.com/chenxiaolong/avbroot/pull/158
[PR #159]: https://github.com/chenxiaolong/avbroot/pull/159
[PR #161]: https://github.com/chenxiaolong/avbroot/pull/161
[PR #162]: https://github.com/chenxiaolong/avbroot/pull/162
[PR #163]: https://github.com/chenxiaolong/avbroot/pull/163
[PR #164]: https://github.com/chenxiaolong/avbroot/pull/164
[PR #165]: https://github.com/chenxiaolong/avbroot/pull/165
[PR #167]: https://github.com/chenxiaolong/avbroot/pull/167
[PR #168]: https://github.com/chenxiaolong/avbroot/pull/168
[PR #169]: https://github.com/chenxiaolong/avbroot/pull/169
[PR #170]: https://github.com/chenxiaolong/avbroot/pull/170
[PR #171]: https://github.com/chenxiaolong/avbroot/pull/171
[PR #172]: https://github.com/chenxiaolong/avbroot/pull/172
[PR #173]: https://github.com/chenxiaolong/avbroot/pull/173
[PR #174]: https://github.com/chenxiaolong/avbroot/pull/174
[PR #175]: https://github.com/chenxiaolong/avbroot/pull/175
[PR #176]: https://github.com/chenxiaolong/avbroot/pull/176
[PR #178]: https://github.com/chenxiaolong/avbroot/pull/178
[PR #181]: https://github.com/chenxiaolong/avbroot/pull/181
[PR #182]: https://github.com/chenxiaolong/avbroot/pull/182
[PR #183]: https://github.com/chenxiaolong/avbroot/pull/183
[PR #184]: https://github.com/chenxiaolong/avbroot/pull/184
[PR #188]: https://github.com/chenxiaolong/avbroot/pull/188
[PR #189]: https://github.com/chenxiaolong/avbroot/pull/189
[PR #190]: https://github.com/chenxiaolong/avbroot/pull/190
[PR #191]: https://github.com/chenxiaolong/avbroot/pull/191
[PR #196]: https://github.com/chenxiaolong/avbroot/pull/196
[PR #197]: https://github.com/chenxiaolong/avbroot/pull/197
[PR #199]: https://github.com/chenxiaolong/avbroot/pull/199
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
[PR #210]: https://github.com/chenxiaolong/avbroot/pull/210
[PR #211]: https://github.com/chenxiaolong/avbroot/pull/211
[PR #214]: https://github.com/chenxiaolong/avbroot/pull/214
[PR #219]: https://github.com/chenxiaolong/avbroot/pull/219
[PR #221]: https://github.com/chenxiaolong/avbroot/pull/221
[PR #224]: https://github.com/chenxiaolong/avbroot/pull/224
[PR #226]: https://github.com/chenxiaolong/avbroot/pull/226
[PR #227]: https://github.com/chenxiaolong/avbroot/pull/227
[PR #228]: https://github.com/chenxiaolong/avbroot/pull/228
[PR #229]: https://github.com/chenxiaolong/avbroot/pull/229
[PR #230]: https://github.com/chenxiaolong/avbroot/pull/230
[PR #231]: https://github.com/chenxiaolong/avbroot/pull/231
[PR #232]: https://github.com/chenxiaolong/avbroot/pull/232
[PR #233]: https://github.com/chenxiaolong/avbroot/pull/233
[PR #234]: https://github.com/chenxiaolong/avbroot/pull/234
[PR #237]: https://github.com/chenxiaolong/avbroot/pull/237
[PR #240]: https://github.com/chenxiaolong/avbroot/pull/240
[PR #241]: https://github.com/chenxiaolong/avbroot/pull/241
[PR #243]: https://github.com/chenxiaolong/avbroot/pull/243
[PR #244]: https://github.com/chenxiaolong/avbroot/pull/244
[PR #245]: https://github.com/chenxiaolong/avbroot/pull/245
[PR #246]: https://github.com/chenxiaolong/avbroot/pull/246
[PR #247]: https://github.com/chenxiaolong/avbroot/pull/247
[PR #251]: https://github.com/chenxiaolong/avbroot/pull/251
[PR #253]: https://github.com/chenxiaolong/avbroot/pull/253
[PR #255]: https://github.com/chenxiaolong/avbroot/pull/255
[PR #256]: https://github.com/chenxiaolong/avbroot/pull/256
[PR #257]: https://github.com/chenxiaolong/avbroot/pull/257
[PR #261]: https://github.com/chenxiaolong/avbroot/pull/261
[PR #268]: https://github.com/chenxiaolong/avbroot/pull/268
[PR #276]: https://github.com/chenxiaolong/avbroot/pull/276
[PR #277]: https://github.com/chenxiaolong/avbroot/pull/277
[PR #279]: https://github.com/chenxiaolong/avbroot/pull/279
[PR #287]: https://github.com/chenxiaolong/avbroot/pull/287
[PR #288]: https://github.com/chenxiaolong/avbroot/pull/288
[PR #289]: https://github.com/chenxiaolong/avbroot/pull/289
[PR #293]: https://github.com/chenxiaolong/avbroot/pull/293
[PR #296]: https://github.com/chenxiaolong/avbroot/pull/296
[PR #297]: https://github.com/chenxiaolong/avbroot/pull/297
[PR #304]: https://github.com/chenxiaolong/avbroot/pull/304
[PR #307]: https://github.com/chenxiaolong/avbroot/pull/307
[PR #309]: https://github.com/chenxiaolong/avbroot/pull/309
[PR #311]: https://github.com/chenxiaolong/avbroot/pull/311
[PR #312]: https://github.com/chenxiaolong/avbroot/pull/312
[PR #321]: https://github.com/chenxiaolong/avbroot/pull/321
[PR #323]: https://github.com/chenxiaolong/avbroot/pull/323
[PR #329]: https://github.com/chenxiaolong/avbroot/pull/329
[PR #331]: https://github.com/chenxiaolong/avbroot/pull/331
[PR #333]: https://github.com/chenxiaolong/avbroot/pull/333
[PR #334]: https://github.com/chenxiaolong/avbroot/pull/334
[PR #335]: https://github.com/chenxiaolong/avbroot/pull/335
[PR #336]: https://github.com/chenxiaolong/avbroot/pull/336
[PR #337]: https://github.com/chenxiaolong/avbroot/pull/337
[PR #342]: https://github.com/chenxiaolong/avbroot/pull/342
[PR #343]: https://github.com/chenxiaolong/avbroot/pull/343
[PR #347]: https://github.com/chenxiaolong/avbroot/pull/347
[PR #354]: https://github.com/chenxiaolong/avbroot/pull/354
[PR #355]: https://github.com/chenxiaolong/avbroot/pull/355
[PR #357]: https://github.com/chenxiaolong/avbroot/pull/357
[PR #362]: https://github.com/chenxiaolong/avbroot/pull/362
[PR #363]: https://github.com/chenxiaolong/avbroot/pull/363
[PR #364]: https://github.com/chenxiaolong/avbroot/pull/364
[PR #367]: https://github.com/chenxiaolong/avbroot/pull/367
[PR #368]: https://github.com/chenxiaolong/avbroot/pull/368
[PR #369]: https://github.com/chenxiaolong/avbroot/pull/369
[PR #370]: https://github.com/chenxiaolong/avbroot/pull/370
[PR #371]: https://github.com/chenxiaolong/avbroot/pull/371
[PR #373]: https://github.com/chenxiaolong/avbroot/pull/373
[PR #374]: https://github.com/chenxiaolong/avbroot/pull/374
[PR #376]: https://github.com/chenxiaolong/avbroot/pull/376
[PR #377]: https://github.com/chenxiaolong/avbroot/pull/377
[PR #384]: https://github.com/chenxiaolong/avbroot/pull/384
[PR #385]: https://github.com/chenxiaolong/avbroot/pull/385
[PR #386]: https://github.com/chenxiaolong/avbroot/pull/386
[PR #390]: https://github.com/chenxiaolong/avbroot/pull/390
[PR #391]: https://github.com/chenxiaolong/avbroot/pull/391
[PR #392]: https://github.com/chenxiaolong/avbroot/pull/392
[PR #394]: https://github.com/chenxiaolong/avbroot/pull/394
[PR #395]: https://github.com/chenxiaolong/avbroot/pull/395
[PR #397]: https://github.com/chenxiaolong/avbroot/pull/397
[PR #398]: https://github.com/chenxiaolong/avbroot/pull/398
[PR #401]: https://github.com/chenxiaolong/avbroot/pull/401
[PR #402]: https://github.com/chenxiaolong/avbroot/pull/402
[PR #403]: https://github.com/chenxiaolong/avbroot/pull/403
[PR #404]: https://github.com/chenxiaolong/avbroot/pull/404
[PR #408]: https://github.com/chenxiaolong/avbroot/pull/408
[PR #409]: https://github.com/chenxiaolong/avbroot/pull/409
[PR #410]: https://github.com/chenxiaolong/avbroot/pull/410
[PR #411]: https://github.com/chenxiaolong/avbroot/pull/411
[PR #415]: https://github.com/chenxiaolong/avbroot/pull/415
[PR #418]: https://github.com/chenxiaolong/avbroot/pull/418
[PR #421]: https://github.com/chenxiaolong/avbroot/pull/421
[PR #422]: https://github.com/chenxiaolong/avbroot/pull/422
[PR #423]: https://github.com/chenxiaolong/avbroot/pull/423
[PR #424]: https://github.com/chenxiaolong/avbroot/pull/424
[PR #425]: https://github.com/chenxiaolong/avbroot/pull/425
[PR #427]: https://github.com/chenxiaolong/avbroot/pull/427
[PR #428]: https://github.com/chenxiaolong/avbroot/pull/428
[PR #429]: https://github.com/chenxiaolong/avbroot/pull/429
[PR #430]: https://github.com/chenxiaolong/avbroot/pull/430
[PR #434]: https://github.com/chenxiaolong/avbroot/pull/434
[PR #435]: https://github.com/chenxiaolong/avbroot/pull/435
[PR #437]: https://github.com/chenxiaolong/avbroot/pull/437
[PR #438]: https://github.com/chenxiaolong/avbroot/pull/438
[PR #439]: https://github.com/chenxiaolong/avbroot/pull/439
[PR #442]: https://github.com/chenxiaolong/avbroot/pull/442
[PR #443]: https://github.com/chenxiaolong/avbroot/pull/443
[PR #444]: https://github.com/chenxiaolong/avbroot/pull/444
[PR #445]: https://github.com/chenxiaolong/avbroot/pull/445
[PR #446]: https://github.com/chenxiaolong/avbroot/pull/446
[PR #448]: https://github.com/chenxiaolong/avbroot/pull/448
[PR #449]: https://github.com/chenxiaolong/avbroot/pull/449
[PR #452]: https://github.com/chenxiaolong/avbroot/pull/452
[PR #453]: https://github.com/chenxiaolong/avbroot/pull/453
[PR #463]: https://github.com/chenxiaolong/avbroot/pull/463
[PR #464]: https://github.com/chenxiaolong/avbroot/pull/464
[PR #467]: https://github.com/chenxiaolong/avbroot/pull/467
[PR #468]: https://github.com/chenxiaolong/avbroot/pull/468
[PR #470]: https://github.com/chenxiaolong/avbroot/pull/470
[PR #473]: https://github.com/chenxiaolong/avbroot/pull/473
[PR #474]: https://github.com/chenxiaolong/avbroot/pull/474
[PR #475]: https://github.com/chenxiaolong/avbroot/pull/475
Generated
+2306
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
[workspace]
default-members = ["avbroot"]
members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.18.0"
license = "GPL-3.0-only"
edition = "2024"
repository = "https://github.com/chenxiaolong/avbroot"
[workspace.lints.clippy]
cast_lossless = "deny"
missing_fields_in_debug = "warn"
redundant_clone = "deny"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+396
View File
@@ -0,0 +1,396 @@
# 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`.
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
### Repacking an AVB image
```bash
avbroot avb repack -i <input AVB image> -o <output AVB image>
```
This subcommand is equivalent to `avbroot avb unpack` followed by `avbroot avb pack`, except it doesn't need to write any intermediate files to disk.
This is useful for repairing a dm-verify image or for re-signing any image with a specific key.
### Showing vbmeta header and footer information
```bash
avbroot avb 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`.
### Computing vbmeta digest
```bash
avbroot avb digest -i <root vbmeta image>
```
This subcommand computes the vbmeta digest, which is defined as the SHA256 digest of the root vbmeta partition's header, followed by the chained partitions' headers (if any) in the order that they are listed. Chained partitions more than one level deep are ignored.
This digest is equal to the value of the `ro.boot.vbmeta.digest` property or the `RootOfTrust.verifiedBootHash` hardware attestation field.
## `avbroot boot`
### Unpacking a boot image
```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.
## `avbroot lp`
This set of commands is for working with LP (logical partition) images. These are the containers for dynamically-allocated partitions, like `system`. All LP images are supported:
* Empty images: These are the `super_empty.img` images in the factory images for newer Google Pixel devices. They define the layout of the `super` partition, but don't contain any actual data. They also do not contain a backup copy of the metadata. As an optimization, `fastboot` can fill in the actual data during flashing to avoid needing to reboot to fastbootd mode.
* Normal images backed by a single device: These are standalone `super.img` images and are how logical partitions are physically stored on disk in most newer devices. They contain a backup copy of all metadata as well as actual partition data.
* Normal images backed by multiple devices: These are images split across multiple files/partitions and are used on devices where support for LP was retrofitted. For example, the LP setup on newer Android builds for the Google Pixel 3a XL reuse the legacy `system` and `vendor` partitions because there is no `super` partition. These are similar to the single-file LP setups, except that data can be stored across all of the LP images. However, the metadata is only stored on the first LP image.
### Unpacking an LP image
```bash
avbroot lp unpack -i <input LP image> [-i <input LP image>]...
```
This subcommand unpacks the LP metadata to `lp.toml` and the partition images to the `lp_images` directory (for normal images).
If there are multiple images, they must be specified in order. If the order is not known, run `avbroot lp info` on each of the images. The one that successfully parses is the first image and the `block_devices` field in the output specifies the full ordering.
An LP image can have multiple slots. If the LP image originated from a factory image or OTA, all slots are likely identical. If the LP image was dumped from a real device that installed OTA updates in the past, the slots may differ. If the slots are not identical, then the `--slot` option is required to specify which slot to unpack.
### Packing an LP image
```bash
avbroot lp pack -o <output LP image> [-o <output LP image>]...
```
This subcommand packs a new LP image from the `lp.toml` file and `lp_images` directory (for normal images). Any `.img` files in the `lp_images` directory that don't have a corresponding entry in `lp.toml` are silently ignored.
All metadata slots in the newly packed LP image will be identical.
### Repacking an LP image
```bash
avbroot lp repack -i <input LP image> [-i <input LP image>]... -o <output LP image> [-o <output LP image>]...
```
This subcommand is logically equivalent to `avbroot lp unpack` followed by `avbroot lp pack`, except more efficient. Instead of unpacking and packing all partition images, the raw data is directly copied from the old LP image to the new LP image.
When `--slot` is specified, this is useful for discarding unwanted metadata slots and the partition data exclusive to them.
### Showing LP image metadata
```bash
avbroot lp info -i <first LP image>
```
This subcommand shows the LP image metadata, including all metadata slots. If there are multiple images, only the first one is needed because it is the only one that stores the metadata.
## `avbroot payload`
This set of commands is for working with payload binary files (`payload.bin`). The `unpack` and `pack` commands can only work with full payloads because they require the complete data to be available, but the `repack` and `info` commands also work with delta payloads.
### Unpacking a payload binary
```bash
avbroot payload unpack -i <input payload>
```
This subcommand unpacks the payload header information to `payload.toml` and the partition images to the `payload_images` directory.
Only full payload binaries can be unpacked. Delta payload binaries from incremental OTAs are not supported.
### Packing a payload binary
```bash
avbroot payload pack -o <output payload> --key <OTA private key>
```
This subcommand packs a new payload binary from the `payload.toml` file and `payload_images` directory. Any `.img` files in the `payload_images` directory that don't have a corresponding entry in `payload.toml` are silently ignored.
Packing a payload binary requires compressing all of the partition images, which is very CPU intensive. If re-signing an existing payload binary without making any other modifications is all that's needed, use the `repack` subcommand instead.
### Repacking a payload binary
```bash
avbroot payload repack -i <input payload> -o <output payload> --key <OTA private key>
```
This subcommand is logically equivalent to `avbroot payload unpack` followed by `avbroot payload pack`, except significantly more efficient. Instead of decompressing and recompressing all partition images, the raw data is directly copied from the input payload binary.
This is useful for re-signing a payload binary without making any other changes.
### Showing payload header information
```bash
avbroot payload info -i <payload>
```
This subcommand shows all of the payload header fields (which will likely be extremely long).
## `avbroot sparse`
This set of commands is for working with Android sparse images. All features of the file format are supported, including hole chunks and CRC32 checksums.
### Unpacking a sparse image
```bash
avbroot sparse unpack -o <input sparse image> -o <output raw image>
```
This subcommand unpacks a sparse image to a raw image. If the sparse image contains CRC32 checksums, they will be validated during unpacking. If the sparse image contains holes, the output image will be created as a native sparse file.
Certain fastboot factory images may have multiple sparse images, like `super_1.img`, `super_2.img`, etc., where they all touch a disjoint set of regions on the same partition. These can be unpacked by running this subcommand for each sparse image and specifying the `--preserve` option along with using the same output file. This preserves the existing data in the output file when unpacking each sparse image.
### Packing a sparse image
```bash
avbroot sparse pack -i <input raw image> -o <output sparse image>
```
This subcommand packs a new sparse image from a raw image. The default block size is 4096 bytes, which can be changed with the `--block-size` option.
By default, this will pack the entire input file. However, on Linux, there is an optimization where all holes in the input file, if it is a native sparse file, will be stored as hole chunks instead of `0`-filled chunks in the output sparse image.
To pack a partial sparse image, such as those used in the special fastboot factory images mentioned above, pass in `--region <start> <end>`. This option can be specified multiple times to pack multiple regions.
Unlike AOSP's `img2simg` tool, which never writes CRC32 checksums, this subcommand will write checksums if the input file has no holes and the entire file is being packed.
### Repacking a sparse image
```bash
avbroot sparse repack -i <input sparse image> -o <output sparse image>
```
This subcommand is logically equivalent to `avbroot sparse unpack` followed by `avbroot sparse pack`, except more efficient. This is useful for roundtrip testing of avbroot's sparse file parser.
### Showing sparse image metadata
```bash
avbroot sparse info -i <input sparse image>
```
This subcommand shows the sparse image metadata, including the header and all chunks.
+426 -174
View File
@@ -1,242 +1,312 @@
# 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 tool for modifying Android A/B OTA images reproducibly and re-signing them with custom keys. It also includes a [collection of subcommands](./README.extra.md) for packing and unpacking numerous Android image formats.
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
## Requirements
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
* `payload.bin` exists
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command.
A list of devices known to work can be found in the issue tracker at [#299](https://github.com/chenxiaolong/avbroot/issues/299).
## Patches
avbroot applies two patches to the boot images:
avbroot applies the following patches to the partition images:
* Magisk is applied to the `boot` or `init_boot` image, depending on device, as if it were done from the Magisk app.
* The `boot` or `init_boot` image, depending on device, is patched to enable root access if requested.
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded 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 while rooted.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* `payload.bin` exists
* `META-INF/com/android/metadata.pb` exists
* `META-INF/com/android/metadata` contains the line: `ota-type=AB`
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders support this, as well as most OnePlus devices. Other devices may support it as well, but there's no easy way to check without just trying it.
* Any operation that causes an improperly-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). A couple ways an improperly-signed boot image could be flashed include:
* **Do not ever disable the `OEM unlocking` checkbox when using a locked bootloader with root.** This is critically important. With root access, it is possible to corrupt the running system, for example by zeroing out the boot partition. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* The `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
* Any operation that causes an unsigned or differently-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
* The `Uninstall Magisk` feature in Magisk. If root access is no longer needed, Magisk **must** be removed by repatching the OTA with the `--rootless` option, not via the app.
* Performing a regular (unpatched) A/B OTA update. This can be blocked via a Magisk module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
If the boot image is ever modified, **do not reboot**. [Open an issue](https://github.com/chenxiaolong/avbroot/issues/new) for support and be very clear about what steps were done that lead to the situation. If Android is still running and root access works, it might be possible to recover without wiping and starting over.
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
## Usage
1. Make sure the [caveats listed above](#warnings-and-caveats) are understood. It is possible to hard brick by doing the wrong thing!
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
avbroot is a standalone executable. It does not need to be installed and can be run from anywhere.
3. Follow the steps to [generate signing keys](#generating-keys).
Skip this step if you're updating Android, Magisk, or KernelSU after you've performed an [initial setup](#initial-setup). [Updates](#updates) do not require signing keys since you have already generated them in the initial setup.
4. Patch the OTA zip. The base command is:
```bash
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 encode-avb -k avb.key -o avb_pkmd.bin
```
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery 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 PKCS#8-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. Make sure that the version of fastboot is 34 or newer. Older versions have bugs that prevent the `fastboot flashall` command (required later) from working properly.
```bash
git clone --recursive https://github.com/chenxiaolong/avbroot.git
fastboot --version
```
If the repo is already cloned, run the following command instead to fetch the submodules:
2. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
```bash
git submodule update --init --recursive
fastboot flashing unlock
```
3. Follow the steps to [install dependencies](#installing-dependencies).
3. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
4. Follow the steps to [generate signing keys](#generating-keys).
5. Patch the full OTA ZIP.
4. Extract the partition images from the patched OTA that are different from the original.
```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`.
5. Set the `ANDROID_PRODUCT_OUT` environment variable to the directory containing the extracted files.
For sh/bash/zsh (Linux, macOS, WSL):
```bash
# Flash the boot images that were extracted
for image in extracted/*.img; do
partition=$(basename "${image}")
partition=${partition%.img}
export ANDROID_PRODUCT_OUT=extracted
```
fastboot flash "${partition}" "${image}"
done
For PowerShell (Windows):
# Flash the AVB signing public key
```powershell
$env:ANDROID_PRODUCT_OUT = "extracted"
```
For cmd (Windows):
```bat
set ANDROID_PRODUCT_OUT=extracted
```
6. Flash the partition images that were extracted.
```bash
fastboot flashall --skip-reboot
```
Note that this only flashes the OS partitions. The bootloader and modem/radio partitions are left untouched due to fastboot limitations. If they are not already up to date or if unsure, after fastboot completes, follow the steps in the [updates section](#updates) to sideload the patched OTA once. Sideloading OTAs always ensures that all partitions are up to date.
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
7. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
```bash
fastboot reboot-bootloader
fastboot erase avb_custom_key
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
```
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:
8. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
Install the Magisk or KernelSU app and run the following command:
```bash
adb shell su -c 'dmesg | grep libfs_avb'
```
If AVB is working properly, the following message should be printed out:
```bash
init: [libfs_avb]Returning avb_handle with status: Success
```
10. **[Initial setup only]** Lock the bootloader. This will trigger a data wipe again. **Do not uncheck `OEM unlocking`!**
9. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using [avbroot's `oemunlockonboot` Magisk module](#oemunlockonboot-enable-oem-unlocking-on-every-boot) to automatically ensure OEM unlocking is enabled on every boot.
```bash
fastboot flashing lock
```
Confirm by pressing volume down and then power. Then reboot.
Remember: **Do not uncheck `OEM unlocking`!**
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using the [`OEMUnlockOnBoot` module](https://github.com/chenxiaolong/OEMUnlockOnBoot) to automatically ensure OEM unlocking is enabled on every boot.
10. That's it! To update the OS, Magisk, or KernelSU 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. Generate a new patched OTA by following the steps in the [usage section](#usage).
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. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
3. 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. Restart your phone. Note: the phone will likely take a long time to startup after an OS update (a few minutes in some cases).
avbroot's Magisk modules can be built by running:
**Warning**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
```bash
python modules/build.py
```
The status can be found by running `adb logcat -v color -s update_engine`. Alternatively, if [Custota](https://github.com/chenxiaolong/Custota) is installed (even if it's not configured to point to a custom OTA server), it will show a notification until the snapshot merge operation completes.
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
## Reverting to stock firmware
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.
To stop using avbroot and revert to the stock firmware:
### `clearotacerts`: Blocking A/B OTA Updates
1. Reboot into fastboot mode and unlock the bootloader. This will trigger a data wipe.
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.
2. Erase the custom AVB public key.
The `clearotacerts` module additionally makes A/B OTAs fail while booted into Android to prevent accidental manual updates. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
```bash
fastboot erase avb_custom_key
```
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
3. Flash the stock firmware.
### `oemunlockonboot`: Enable OEM unlocking on every boot
4. That's it! There are no other remnants to clean up.
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.
## OTA updates
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
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.
Disabling the system updater app is recommended to prevent it from even attempting to install an unpatched OTA. To do so:
* Stock OS: Turn off `Automatic system updates` in Android's Developer Options.
* Custom OS: Disable the system updater app (or block its network access) from Settings -> Apps -> See all apps -> (three-dot menu) -> Show system -> (find updater app).
This is especially important for some custom OS's because their system updater app may get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails.
To self-host a custom OTA server, see [Custota](https://github.com/chenxiaolong/Custota).
## Repair mode
Some devices now ship with a Repair Mode feature that boots the system with a fresh `userdata` image so that repair technicians are able to run on-device diagnostics without needing the user's credentials to unlock the device.
When the device is rooted, it is unsafe to use Repair Mode. Unless you are using release builds of Magisk/KernelSU signed with your own keys, it's trivial for someone to just install the Magisk/KernelSU app while in repair mode to gain root access with no authentication.
To safely use Repair Mode:
1. Unroot the device by repatching the OTA with the `--rootless` option (instead of `--magisk` or `--prepatched`) and flashing it.
2. Turn on Repair Mode.
3. After receiving the repaired device, exit Repair Mode.
4. Flash the (rooted) patched OTA as normal.
Because the unrooting and rooting are done by flashing OTAs, the device's data will not be wiped.
## Magisk preinit device
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition's block device name to be specified via `--magisk-preinit-device <name>`. To find the partition name:
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition to be explicitly specified via `--magisk-preinit-device <name>`. To find the partition name:
1. Extract the boot image from the original/unpatched OTA:
```bash
python avbroot.py \
extract \
avbroot ota extract \
--input /path/to/ota.zip \
--directory . \
--boot-only
--partition <name> # init_boot or boot, depending on device
```
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device! 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 +315,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 +325,91 @@ 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.
### Skipping OTA certificate patches
avbroot can skip modifying `otacerts.zip` with the `--skip-system-ota-cert` and `--skip-recovery-ota-cert` options. **Do not use these unless you have a good reason to do so.**
When `--skip-system-ota-cert` is used, the OTA certificates in the `system` partition will not be modified. This prevents custom OTA updater apps from installing further patched OTAs while booted into Android.
When `--skip-recovery-ota-cert` is used, the OTA certificates in the `vendor_boot` or `recovery` partition will not be modified. **This prevents sideloading further patched OTAs from recovery mode.**
If `--skip-recovery-ota-cert` is used because the OTA certificate was already manually added to the boot image, then [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done. The verification process is only capable of checking the boot image's copy of the OTA certificates, not the system image's copy of them.
### Skipping all patches
To have avbroot make the absolute minimal changes:
* Specify `--skip-system-ota-cert`
* Specify `--skip-recovery-ota-cert`
* Specify `--rootless`
* Omit `--dsu`
This will re-sign the `vbmeta` partition and the OTA with the custom keys, but leave all other partitions untouched.
**This should only be used for advanced troubleshooting.** Without the OTA certificate patches, the resulting OTA will not be able to install further updates.
### Replacing partitions
@@ -278,35 +419,53 @@ 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`.
### Changing virtual A/B CoW compression algorithm
The virtual A/B CoW compression algorithm can be changed by passing in `--vabc-algo <algo>` with `gz` or `lz4`. OTAs normally use an algorithm that is compatible with the initial version of Android shipped on the device.
* Devices launching with Android 12 support `gz` and `brotli` (unsupported by avbroot)
* Devices launching with Android 14 support `lz4`
* Devices launching with Android 15 support `zstd` (unsupported by avbroot)
Picking a fast algorithm, like lz4, can speed up OTA installation significantly when installing via a custom OTA updater app. However, there is no performance difference when sideloading an OTA from recovery mode.
Note that the currently running version of Android must support the specified compression algorithm or else the OTA will fail to install. For example, trying to install an Android 14 OTA that uses lz4 CoW compression will fail if the running system is Android 13.
### Non-interactive use
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
* 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 +475,134 @@ 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:
* Use unencrypted private keys. This is strongly discouraged.
```bash
openssl pkcs8 -in avb.key -topk8 -nocrypt -out avb.unencrypted.key
openssl pkcs8 -in ota.key -topk8 -nocrypt -out ota.unencrypted.key
```
### Extracting an OTA
### Extracting the entire OTA
To extract all images contained within the OTA's `payload.bin`, run:
To extract the partition images contained within an OTA's `payload.bin`, run:
```bash
python avbroot.py \
extract \
avbroot ota extract \
--input /path/to/ota.zip \
--directory extracted \
--all
--directory extracted
```
## Implementation Details
By default, this only extracts the images that could potentially be patched by avbroot. To extract all images, use the `--all` option. To extract specific images, use the `--partition <name>` option, which can be specified multiple times.
* 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.
This command also supports extracting the embedded OTA certificate and AVB public key using the `--cert-ota` and `--public-key-avb` options. To extract only these components, pass in `--none` to skip extracting partition images.
* AVB has two methods of handling signature verification:
### Zip write mode
* 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.
By default, avbroot uses streaming writes for the output OTA during patching. This means it computes the sha256 digest for the digital signature as the file is being written. This mode causes the zip file to contain data descriptors, which is part of the zip standard and works on the vast majority of devices. However, some devices may have broken zip file parsers and fail to properly read OTA zip files containing data descriptors. If this is the case, pass in `--zip-mode seekable` when patching.
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).
The seekable mode writes zip files without data descriptors, but as the name implies, requires seeking around the file instead of writing it sequentially. The sha256 digest for the digital signature is computed after the zip file has been fully written.
### Signing with an external program
avbroot supports delegating all RSA signing operations to an external program with the `--signing-helper` option. When using this option, the `--key-avb` and `--key-ota` options must be given a public key instead of a private key.
For each signing operation, avbroot will invoke the program with:
```bash
<helper> <algorithm> <public key>
```
The algorithm is one of `SHA{256,512}_RSA{2048,4096}` and the public key is what was passed to avbroot. The program can use the public key to find the corresponding private key (eg. on a hardware security module). avbroot will write a PKCS#1 v1.5 padded digest to `stdin` and the helper program is expected to perform a raw RSA signing operation and write the raw signature (octet string matching key size) to `stdout`.
By default, this behavior is compatible with the `--signing_helper` option in AOSP's avbtool. However, avbroot additionally extends the arguments to support non-interactive use. If `--pass-{avb,ota}-file` or `--pass-{avb,ota}-env-var` are used, then the helper program will be invoked with two additional arguments that point to the password file or environment variable.
```bash
<helper> <algorithm> <public key> file <pass file>
# or
<helper> <algorithm> <public key> env <env file>
```
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
### 16K page size developer option
On recent devices running Android 16 and newer, there may be an option in Android's developer options to switch to a 16K page size kernel. This will not work when running an avbroot-patched OS. The switch internally works by flashing incremental OTAs:
* `/vendor/boot_otas/boot_ota_16k.zip` to switch to the 16K page size kernel (requires the `boot` partition to be currently flashed with the 4K kernel)
* `/vendor/boot_otas/boot_ota_4k.zip` to switch to the 4K page size kernel (requires the `boot` partition to be currently flashed with the 16K kernel)
These `boot_otas` are unflashable when running an avbroot-patched OS because the `payload.bin` inside of them are signed by the OEM's key. These are also not proper OTA files. They don't contain any OTA metadata and the zip file itself is not signed. It's nothing more than a plain old zip file that stores a signed `payload.bin`.
There are no plans to add support for patching these `boot_otas`. It requires support for modifying filesystems and handling incremental OTAs, both of which are very non-trivial.
Folks who are determined to make this work anyway can try these manual steps to sign these `boot_otas` with your own key. Since the incremental OTAs are not being regenerated, the `boot` partition must be left unmodified when running `avbroot ota patch`.
1. Unpack `vendor.img` with avbroot and [afsr](https://github.com/chenxiaolong/afsr).
```bash
avbroot avb unpack -i vendor.img
afsr unpack -i raw.img
```
2. Extract `payload.bin` from `boot_otas/boot_ota_16k.zip`.
3. Re-sign `payload.bin` with your OTA key.
```bash
avbroot payload repack \
-i payload.bin.orig \
-o payload.bin \
-k ota.key \
--output-properties payload_properties.txt
```
4. Create a new zip of `payload.bin` and `payload_properties.txt`. The files must be stored uncompressed (eg. with `zip -0`).
5. Repeat the procedure for `boot_otas/boot_ota_4k.zip`.
6. Repack `vendor.img` and sign it with your AVB key.
```bash
afsr pack -o raw.img
avbroot avb pack -o vendor.img -k avb.key --recompute-size
```
7. Patch the (normal) OTA with:
```bash
avbroot ota patch \
--replace vendor <modified vendor> \
<normal arguments...>
```
## Building from source
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
```bash
cargo build --release
```
The output binary is written to `target/release/avbroot`.
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
### Android cross-compilation
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
```bash
cargo android build --release --target aarch64-linux-android
```
It is possible to run the tests if the host is running Linux, qemu-user-static is installed, and the executable is built with `RUSTFLAGS=-C target-feature=+crt-static` and `--features static`.
## Verifying digital signatures
To verify the digital signatures of the downloads, follow [the steps here](https://github.com/chenxiaolong/chenxiaolong/blob/master/VERIFY_SSH_SIGNATURES.md).
## Contributing
+596
View File
@@ -0,0 +1,596 @@
# avbroot
avbroot – это утилита для воспроизводимой модификации OTA-образов Android A/B-формата и их переподписания пользовательскими ключами. Она также включает в себя [набор подкоманд](./README.extra.md) для упаковки и распаковки образов Android различных форматов.
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
## Требования
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
* наличие файла `payload.bin` (обычно находится в корне архива)
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это производится с помощью команды `fastboot flash avb_custom_key`.
Список девайсов, на которых проверялась совместимость с указанным выше функционалом, находится здесь: [#299.](https://github.com/chenxiaolong/avbroot/issues/299)
## Патчи
avbroot модифицирует следующие образы:
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа, если это запрашивается.
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
* `system` тоже модифицируется для замены сертификата проверки подписи OTA. Это не позволит системному приложению обновлений ОС установить оригинальный непропатченный OTA и дает возможность использовать сторонние приложения для установки пропатченных OTA.
## Предостережения
* **Всегда оставляйте опцию `Заводской разблокировки`** (или OEM unlocking в англ.) **включенной при наличии root-прав с заблокированным загрузчиком.** Это очень важно. Доступ к root-правам потенциально позволяет перезаписать загрузочный раздел из-под системы, будь то сделано случайно или намеренно, файлом, который не был подписан должным образом. В таком случае, система и режим Recovery больше не смогут загрузиться, а команда `fastboot flashing unlock` будет недоступна, потому что параметр Заводской разблокировки отключен. То есть, это приведет к **_хардбрику устройства_**.
Повторюсь: **_ВСЕГДА оставляйте `Заводскую разблокировку` включенной при наличии root-прав._**
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). К подобным операциям в том числе относятся:
* Метод `Прямой установки` для обновления Magisk. Magisk можно обновлять **только путем репатчинга OTA,** но не через его приложение.
* Функция `Удаление Magisk` в приложении Magisk. Если вам больше не нужен root-доступ, Magisk **должен быть удален путем репатчинга OTA** с использованием параметра `--rootless`, но не через его приложение.
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв Issue,](https://github.com/chenxiaolong/avbroot/issues/new) и четко разъясните, какие конкретные действия привели к возникновению такой ситуации. Если Android всё еще работает и доступ к root-правам сохранился – вероятно, получится откатить изменения до исходного состояния, не стирая ваши данные.
## Использование
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
avbroot – это отдельный исполняемый файл. Он не требует установки и может быть запущен из любого места на диске.
3. [Сгенерируйте ключи подписи.](#генерация-ключей)
4. Пропатчите ОТА-архив с помощью команды:
```bash
avbroot ota patch \
--input /путь/к/ota.zip \
--key-avb /путь/к/avb.key \
--key-ota /путь/к/ota.key \
--cert-ota /путь/к/ota.crt \
```
Добавьте следующие аргументы в конец команды в зависимости от того, как вы хотите получить root-доступ.
* Для получения root-доступа с использованием Magisk:
```bash
--magisk /путь/к/magisk.apk \
--magisk-preinit-device <имя>
```
Если вы не знаете имени раздела предварительной инициализации Magisk, следуйте инструкции [в соответствующем разделе.](#предварительная-инициализация-устройства-для-magisk)
Если вы пропатчили загрузочный образ вручную через приложение Magisk (вместо автоматического идентичного патчинга через avbroot), используйте следующий аргумент:
```bash
--prepatched /путь/к/magisk_patched-xxxxx_yyyyy.img
```
* Для получения root-доступа с использованием KernelSU:
```bash
--prepatched /путь/к/kernelsu_boot.img
```
* Без root-доступа:
```bash
--rootless
```
Больше информации про существующие аргументы можно найти в разделе [расширенного использования.](#расширенное-использование)
Если название для `--output` не указывается, то готовый файл будет записан как `<название-ota-zip-в-input>.patched`.
5. Готово! Для прошивки пропатченного OTA следуйте инструкции в разделе [первоначальной настройки.](#первоначальная-настройка) Для последующих обновлений тоже есть соответствующий [раздел обновлений.](#обновления)
## Генерация ключей
Во время патчинга OTA, avbroot подписывает несколько компонентов:
* загрузочный образ (boot)
* образ vbmeta
* payload из OTA
* сам архив OTA
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
1. Сгенерируйте ключи подписи для AVB и OTA.
```bash
avbroot key generate-key -o avb.key
avbroot key generate-key -o ota.key
```
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
```bash
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
```
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при установке обновления.
```bash
avbroot key generate-cert -k ota.key -o ota.crt
```
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке PKCS#8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
Если вы потеряете ключ(-и) подписи AVB или OTA, вы больше не сможете подписывать новые OTA-архивы. Придется генерировать новые ключи подписи и разблокировать загрузчик (что приведет к стиранию всех данных). В таком случае возвращайтесь к инструкции в разделе [использования.](#использование)
## Первоначальная настройка
1. Убедитесь, что вы используете утилиту fastboot версии 34 или новее. Предыдущие версии содержат баги, что не позволяют команде `fastboot flashall` (которая понадобится по ходу инструкции) работать правильно.
```bash
fastboot --version
```
2. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
```bash
fastboot flashing unlock
```
3. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
4. Извлекаем из пропатченного OTA модифицированные образы:
```bash
avbroot ota extract \
--input /путь/к/ota.zip.patched \
--directory extracted \
--fastboot
```
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
5. Установите переменную окружения `ANDROID_PRODUCT_OUT`, указав директорию с извлеченными файлами.
Для sh/bash/zsh (Linux, macOS, WSL):
```bash
export ANDROID_PRODUCT_OUT=extracted
```
Для PowerShell (Windows):
```powershell
$env:ANDROID_PRODUCT_OUT = "extracted"
```
Для cmd (Командная строка или Терминал) (Windows):
```bat
set ANDROID_PRODUCT_OUT=extracted
```
6. Прошейте извлеченные образы разделов.
```bash
fastboot flashall --skip-reboot
```
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
7. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
```bash
fastboot reboot-bootloader
fastboot erase avb_custom_key
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
```
8. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
Установите приложение Magisk или KernelSU и выполните следующую команду:
```bash
adb shell su -c 'dmesg | grep libfs_avb'
```
Если AVB работает корректно, будет выведено следующее сообщение:
```bash
init: [libfs_avb]Returning avb_handle with status: Success
```
9. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
```bash
fastboot flashing lock
```
Подтвердите нажатием клавиш уменьшения громкости и включения, а после перезагрузитесь в систему.
Напоминаю: **не отключайте `Заводскую разблокировку`!**
**ПРЕДУПРЕЖДЕНИЕ**: Если вы прошили CalyxOS, мастер настройки [автоматически отключит опцию `Заводской разблокировки`.](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140) Не забудьте снова включить её вручную в настройках для разработчиков. Для перестраховки можете использовать [модуль `OEMUnlockOnBoot`,](https://github.com/chenxiaolong/OEMUnlockOnBoot) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
10. Готово! Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
## Обновления
Обновления Android, Magisk и KernelSU выполняются одинаково – исключительно путем обновления или репатчинга того же самого OTA.
1. Если Magisk или KernelSU обновились, сначала установите их новый `.apk`. Если вы случайно открыли приложение после обновления, убедитесь, что оно не начало прошивать загрузочный образ. Если появится предложение обновить сам загрузочный образ – отклоните его.
2. Следуйте инструкции в разделе [использования,](#использование) чтобы пропатчить OTA уже с новым .apk Magisk'а/предварительно пропатченным образом с Magisk или KernelSU.
3. Перезагрузитесь в режим Recovery. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем нажмите кнопку увеличения громкости один раз.
4. Обновитесь (Apply update from adb → `adb sideload <ota.zip.patched>`).
5. Готово!
## Возврат на заводскую прошивку
Если вы хотите отказаться от использования avbroot и вернуться на стоковую прошивку:
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик. Это приведет к стиранию всех пользовательских данных.
2. Удалите пользовательский публичный ключ AVB.
```bash
fastboot erase avb_custom_key
```
3. Прошейте стоковую прошивку. Готово.
## OTA-обновления
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как из-под загруженной системы, так и при прошивке через Recovery.
Рекомендуется отключить системное приложение для обновлений, чтобы оно не пыталось установить непропатченные OTA:
* Стоковая (заводская) прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
Если вы хотите поднять собственный сервер для ОТА-обновлений, вам может быть интересно приложение [Custota.](https://github.com/chenxiaolong/Custota)
## Режим обслуживания
Некоторые устройства поставляются с режимом обслуживания, который загружает систему с чистым образом `userdata`, благодаря чему специалист по ремонту может проводить диагностику устройства, не имея доступа к пользовательским данным владельца.
Если на устройстве есть root-права, использовать этот режим небезопасно. Если у вас обычная сборка Magisk/KernelSU, не подписанная вашим собственным ключом, кто угодно может установить официальное приложение Magisk/KernelSU в режиме обслуживания и запросто получить root-права без какой-либо аутентификации.
Потому, чтобы безопасно использовать режим обслуживания:
1. Отключите root-доступ на устройстве, пропатчив OTA с аргументом `--rootless` (вместо `--magisk` или `--prepatched`) и прошив его.
2. Включите режим обслуживания.
3. Получив отремонтированное устройство обратно, выйдите из режима обслуживания.
4. Прошейте рутированный OTA в обычном режиме.
Поскольку удаление root-прав и повторное их получение выполняются путем перепрошивки OTA, данные устройства стёрты не будут.
## Предварительная инициализация устройства для Magisk
Magisk версии 25211 и новее требует наличие раздела, доступного для записи пользовательских правил SELinux, к которым необходимо обращаться на ранних этапах загрузки. Его можно определить только на реальном устройстве, поэтому avbroot требует указания точного названия с помощью аргумента `--magisk-preinit-device <имя>`. Чтобы получить имя раздела:
1. Извлеките загрузочный образ из оригинального, непропатченного OTA:
```bash
avbroot ota extract \
--input /path/to/ota.zip \
--directory . \
--boot-only
--partition <название раздела> # init_boot или boot, в зависимости от устройства
```
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
Приложение Magisk выведет в лог строку, подобную следующей:
```
- Pre-init storage partition device ID: <имя>
```
Также и avbroot может вывести информацию о разделе, обнаруженном Magisk, для этого выполните команду:
```bash
avbroot boot magisk-info \
--image magisk_patched-*.img
```
Имя раздела будет выведено как: `PREINITDEVICE=<имя>`.
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
## Проверка OTA
Чтобы проверить все подписи и хэши, связанные с установкой OTA и процессом загрузки AVB, выполните команду:
```bash
avbroot ota verify \
--input /путь/к/ota.zip \
--cert-ota /путь/к/ota.crt \
--public-key-avb /путь/к/avb_pkmd.bin
```
Эта команда работает для любого OTA, независимо от того, пропатчено оно или нет.
Если опции `--cert-ota` и `--public-key-avb` не указаны, то подписи проверяются только на корректность, не проверяя, совпадают ли они внутри всех файлов.
## Подсказки через Tab
Поскольку avbroot имеет множество опций, будет удобно настроить подсказки с автозаполнением для используемой оболочки. Конфигурации генерируются в самом avbroot.
#### bash
Добавьте в `~/.bashrc`:
```bash
eval "$(avbroot completion -s bash)"
```
#### zsh
Добавьте в `~/.zshrc`:
```bash
eval "$(avbroot completion -s zsh)"
```
#### fish
Добавьте в `~/.config/fish/config.fish`:
```bash
avbroot completion -s fish | source
```
#### PowerShell
Добавьте в загрузочный скрипт PowerShell (`profile.ps1`):
```powershell
Invoke-Expression (& avbroot completion -s powershell)
```
## Расширенное использование
### Использование заранее пропатченного boot.img
avbroot может подменить используемый загрузочный образ на заранее пропатченный (вместо того, чтобы самостоятельно применять патч). Это пригодится в случае, если у вас уже имеется пропатченный через приложение Magisk образ ядра или образ с поддержкой KernelSU. Для этого используйте аргумент `--prepatched <загрузочный образ>` вместо `--magisk <apk>`. То есть, указав `--prepatched`, avbroot пропустит применение патчинга Magisk'ом, но по-прежнему применит патч OTA-сертификата.
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
### Пропуск патча для root-доступа
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
### Пропуск патчинга сертификата OTA
Вы можете пропустить изменение otacerts.zip, используя аргументы `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.**
При использовании `--skip-system-ota-cert`, сертификаты OTA в образе `system` изменены не будут. Это не позволит сторонним приложениям для OTA-обновлений устанавливать будущие пропатченные OTA из-под загруженной системы.
При использовании `--skip-recovery-ota-cert`, сертификаты OTA в образах `vendor_boot` или `recovery` изменены не будут. **Это не позволит устанавливать будущие пропатченные OTA в режиме Recovery.**
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
### Пропуск всех патчей
Чтобы внести самый минимум изменений, укажите аргументы:
* `--skip-system-ota-cert`
* `--skip-recovery-ota-cert`
* `--rootless`
* не используйте аргумент `--dsu`.
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и OTA, остальные разделы останутся нетронутыми.
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
### Подмена образов
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, используя аргумент `--replace <имя раздела> /путь/к/образу.img`.
Единственное, что меняется – это то, откуда считывается файл. При использовании `--replace` вместо образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
### Очистка флагов vbmeta
Некоторые сборки Android-прошивок могут поставляться с образом `vbmeta`, в котором флаги установлены таким образом, что AVB фактически отключен. Если avbroot сталкивается с такими образом, процесс патчинга завершается ошибкой с сообщением следующего типа:
```
Verified boot is disabled by vbmeta's header flags: 0x3
```
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
### Изменение алгоритма CoW сжатия для вирутального A/B
Алгоритм CoW (copy-on-write) сжатия для виртуального A/B можно изменить, используя аргумент `--vabc-algo <алгоритм>`, указав `gz` или `lz4`. Как правило, по умолчанию OTA использует алгоритм, который совместим с изначальной версией Android, на которой поставлялось устройство.
* Девайсы, поставляемые с Android 12, поддерживают `gz` и `brotli` (последний не поддерживается avbroot)
* Девайсы, поставляемые с Android 14, поддерживают `lz4`
* Девайсы, поставляемые с Android 15, поддерживают `zstd` (не поддерживается avbroot)
Выбор быстрого алгоритма, такого как lz4, может значительно ускорить установку OTA из-под системы (при использованием стороннего приложения для OTA-обновлений). Однако, при установке OTA в режиме Recovery, разницы в скорости не будет.
Обратите внимание, что текущая используемая версия Android должна поддерживать выбранный алгоритм сжатия. В противном случае установка завершится ошибкой. Например, попытка установить OTA-обновление с Android 14, использующее алгоритм lz4, приведет к ошибке, если установка производится из-под Android 13.
### Использование в неинтерактивном режиме
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
* Предоставить пароли через файлы:
```bash
avbroot ota patch \
--pass-avb-file /путь/к/avb.passphrase \
--pass-ota-file /путь/к/ota.passphrase \
<...>
```
На Unix-подобных системах "файлы" могут быть каналами ("pipes"). В оболочках, поддерживающих подстановку процесса (bash, zsh и т. д.), пароль можно запросить с помощью команды (например, запрашивая у менеджера паролей).
```bash
avbroot ota patch \
--pass-avb-file <(команда для запроса пароля AVB) \
--pass-ota-file <(команда для запроса пароля OTA) \
<...>
```
* Предоставить пароли через переменные среды. Это менее безопасно, поскольку любой процесс, запущенный от имени того же пользователя, может видеть значения переменных среды.
```bash
export PASSPHRASE_AVB="пароль AVB"
export PASSPHRASE_OTA="пароль OTA"
avbroot ota patch \
--pass-avb-env-var PASSPHRASE_AVB \
--pass-ota-env-var PASSPHRASE_OTA \
<...>
```
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
### Извлечение образов из OTA
Чтобы извлечь образы разделов, содержащихся в `payload.bin`, используйте команду:
```bash
avbroot ota extract \
--input /путь/к/ota.zip \
--directory extracted
```
По умолчанию извлекаются только те образы, которые потенциально могут быть пропатчены с помощью avbroot. Чтобы извлечь все образы, используйте опцию `--all`. Для извлечения конкретных образов используйте опцию `--partition <название раздела>`, которую можно указать несколько раз.
Эта команда также поддерживает извлечение встроенного сертификата OTA и публичного ключа AVB с помощью опций `--cert-ota` и `--public-key-avb`. Чтобы извлечь только эти компоненты, укажите аргумент `--none`, чтобы пропустить извлечение образов разделов.
### Режим записи ZIP
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
Режим seekable записывает ZIP-файлы без описателей данных, но, как следует из названия, требует перемещения по файлу, вместо последовательной записи. Дайджест sha256 для цифровой подписи вычисляется после того, как ZIP-файл был полностью записан.
### Подписание с использованием внешней программы
avbroot поддерживает делегирование всех операций подписания RSA внешней программе с помощью опции `--signing-helper`. При использовании этой опции, для `--key-avb` и `--key-ota` должен быть указан публичный ключ вместо приватного.
Для каждой операции подписания, avbroot будет вызывать программу с параметрами:
```bash
<helper> <algorithm> <public key>
```
Алгоритм (`<algorithm>`) — это один из `SHA{256,512}_RSA{2048,4096}`, а публичный ключ (`<public key>`) — это тот, что был передан в avbroot. Внешняя программа может использовать публичный ключ для поиска соответствующего приватного ключа (например, на аппаратном модуле безопасности). avbroot запишет дайджест, отформатированный по PKCS#1 v1.5, в `stdin`, а внешняя программа должна выполнить операцию сырого подписания RSA и записать сырую подпись (октетная строка, соответствующая размеру ключа) в `stdout`.
По умолчанию, это поведение совместимо с опцией `--signing_helper` в avbtool от AOSP. Однако avbroot дополнительно расширяет аргументы для поддержки неинтерактивного использования. Если используются опции `--pass-{avb,ota}-file` или `--pass-{avb,ota}-env-var`, то внешняя программа будет вызвана с двумя дополнительными аргументами, указывающими на файл пароля или переменную окружения.
```bash
<helper> <algorithm> <public key> file <pass file>
# или
<helper> <algorithm> <public key> env <env file>
```
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
### Размер страницы 16 КБ в настройках для разработчиков
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
Поддержка `boot_otas` не планируется. Это потребует реализации функционала для модификации ФС в инкрементальных OTA и их дальнейшей обработки, что сделать очень непросто.
Если вы всё же хотите завести эту функцию, можно попробовать вручную подписать файлы в `boot_otas` собственным ключом. Поскольку инкрементальные OTA не пересоздаются, раздел `boot` должен оставаться без изменений во время выполнения команды `avbroot ota patch`.
1. Распакуйте `vendor.img` с помощью avbroot и [afsr](https://github.com/chenxiaolong/afsr):
```bash
avbroot avb unpack -i vendor.img
afsr unpack -i raw.img
```
2. Извлеките `payload.bin` из `boot_otas/boot_ota_16k.zip`.
3. Переподпишите `payload.bin` вашим OTA-ключом:
```bash
avbroot payload repack \
-i payload.bin.orig \
-o payload.bin \
-k ota.key \
--output-properties payload_properties.txt
```
4. Создайте новый zip, включающий `payload.bin` и `payload_properties.txt`. Файлы должны быть добавлены без сжатия (например, с помощью `zip -0`).
5. Повторите эту процедуру для `boot_otas/boot_ota_4k.zip`.
6. Соберите `vendor.img` обратно и подпишите его вашим AVB-ключом:
```bash
afsr pack -o raw.img
avbroot avb pack -o vendor.img -k avb.key --recompute-size
```
7. Пропатчите обычный OTA-архив с прошивкой, подменив `vendor` на модифицированный образ:
```bash
avbroot ota patch \
--replace vendor <модифицированный vendor.img> \
<дальше указываются аргументы, как при обычном патчинге>
```
## Сборка из исходного кода
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
```bash
cargo build --release
```
Исполняемый файл будет записан в `target/release/avbroot`.
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
### Кросс-компиляция на Android
Чтобы использовать кросс-компиляцию на Android, установите [cargo-android](https://github.com/chenxiaolong/cargo-android) и воспользуйтесь оболочкой `cargo android`. Чтобы создать релизную сборку для aarch64, выполните:
```bash
cargo android build --release --target aarch64-linux-android
```
Возможно выполнение тестов, если хост работает под управлением Linux, установлен qemu-user-static, а исполняемый файл собран с `RUSTFLAGS=-C target-feature=+crt-static` и `--features static`.
## Проверка цифровых подписей
Чтобы проверить цифровые подписи, [следуйте этой инструкции.](https://github.com/chenxiaolong/chenxiaolong/blob/master/VERIFY_SSH_SIGNATURES.md)
## Вклад
Буду рад вашему вкладу в разработку! Однако я вряд ли приму изменения для поддержки устройств, которые ведут себя значительно иначе, чем устройства Pixel.
## Лицензия
avbroot распространяется по лицензии GPLv3. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
+7
View File
@@ -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.
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env python3
from avbroot import main
if __name__ == '__main__':
main.main()
+88
View File
@@ -0,0 +1,88 @@
[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 = { version = "2.4.1", features = ["serde"] }
bstr = "1.6.2"
bzip2 = "0.6.0"
cap-std = "3.0.0"
cap-tempfile = "3.0.0"
clap = { version = "4.4.1", features = ["derive"] }
clap_complete = "4.4.0"
cms = { version = "0.2.2", features = ["std"] }
# We can't upgrade to 0.10.0 until x509-cert updates it too, since it's part of
# the public API.
const-oid = "0.9.5"
crc32fast = "1.4.2"
ctrlc = "3.4.0"
dlv-list = "0.6.0"
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.4.1"
lz4_flex = "0.11.1"
memchr = "2.6.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
passterm = "2.0.3"
phf = { version = "0.12.1", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
prost = "0.14.1"
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
rand = "0.8.5"
rayon = "1.7.0"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
# 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.14"
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
serde = { version = "1.0.188", features = ["derive"] }
sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "2.0.3"
toml_edit = { version = "0.23.3", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = { version = "0.2.4", features = ["builder"] }
zerocopy = { version = "0.8.10", features = ["std"] }
zerocopy-derive = "0.8.5"
# https://github.com/zip-rs/zip2/pull/367
# https://github.com/zip-rs/zip2/pull/368
# For getting the data offset when writing new zip entries.
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip2"
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
default-features = false
features = ["deflate"]
[target.'cfg(unix)'.dependencies]
libc = "0.2.158"
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
[build-dependencies]
constcat = "0.6.0"
prost-build = "0.14.1"
protox = "0.9.0"
[dev-dependencies]
assert_matches = "1.5.0"
[features]
static = ["liblzma/static"]
[lints]
workspace = true
-13
View File
@@ -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
View File
@@ -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,
)
+89
View File
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2023-2024 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();
const CUE_AI: &str = ".chromeos_update_engine.ApexInfo";
const CUE_DAM: &str = ".chromeos_update_engine.DeltaArchiveManifest";
const CUE_DPG: &str = ".chromeos_update_engine.DynamicPartitionGroup";
const CUE_DPM: &str = ".chromeos_update_engine.DynamicPartitionMetadata";
const CUE_PU: &str = ".chromeos_update_engine.PartitionUpdate";
const CUE_VABCFS: &str = ".chromeos_update_engine.VABCFeatureSet";
const DERIVE_SERDE: &str = "#[derive(serde::Deserialize, serde::Serialize)]";
const SERDE_DEFAULT: &str = "#[serde(default)]";
const SERDE_SKIP: &str = "#[serde(skip)]";
const SERDE_SKIP_IF_VEC_EMPTY: &str = "#[serde(skip_serializing_if = \"Vec::is_empty\")]";
use constcat::concat as c;
prost_build::Config::new()
.btree_map(["."])
// Allow deserializing and serializing the types we care about.
.type_attribute(CUE_AI, DERIVE_SERDE)
.type_attribute(CUE_DAM, DERIVE_SERDE)
.type_attribute(CUE_DPG, DERIVE_SERDE)
.type_attribute(CUE_DPM, DERIVE_SERDE)
.type_attribute(CUE_PU, DERIVE_SERDE)
.type_attribute(CUE_VABCFS, DERIVE_SERDE)
// Allow default-initializing all fields.
.type_attribute(CUE_AI, SERDE_DEFAULT)
.type_attribute(CUE_DAM, SERDE_DEFAULT)
.type_attribute(CUE_DPG, SERDE_DEFAULT)
.type_attribute(CUE_DPM, SERDE_DEFAULT)
.type_attribute(CUE_PU, SERDE_DEFAULT)
.type_attribute(CUE_VABCFS, SERDE_DEFAULT)
// Don't serialize fields that define the structure of the payload
// binary and that we recompute during packing.
.field_attribute(c!(CUE_DAM, ".signatures_offset"), SERDE_SKIP)
.field_attribute(c!(CUE_DAM, ".signatures_size"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".operations"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".estimate_cow_size"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".old_partition_info"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".new_partition_info"), SERDE_SKIP)
// Don't serialize AVB 1.0 fields.
.field_attribute(c!(CUE_PU, ".hash_tree_data_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".hash_tree_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".hash_tree_algorithm"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".hash_tree_salt"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".fec_data_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".fec_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".fec_roots"), SERDE_SKIP)
// Don't serialize fields for incremental OTAs.
.field_attribute(c!(CUE_PU, ".merge_operations"), SERDE_SKIP)
// Don't serialize fields for vendor-signed images, which update_engine
// doesn't support anyway.
.field_attribute(c!(CUE_PU, ".new_partition_signature"), SERDE_SKIP)
// Don't serialize empty lists.
.field_attribute(c!(CUE_DAM, ".apex_info"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DAM, ".partitions"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DPG, ".partition_names"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DPM, ".groups"), SERDE_SKIP_IF_VEC_EMPTY)
.compile_fds(file_descriptors)
.unwrap();
}
-771
View File
@@ -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')
-187
View File
@@ -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)
-282
View File
@@ -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)
-47
View File
@@ -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
View File
@@ -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()
-222
View File
@@ -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
View File
@@ -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
+115
View File
@@ -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;
}
+445
View File
@@ -0,0 +1,445 @@
//
// 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;
// Information about the cow used by Cow Writer to specify
// number of cow operations to be written
optional uint64 estimate_op_count_max = 20;
}
message DynamicPartitionGroup {
// 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;
// Max bytes to be compressed at once during ota. Options: 4k, 8k, 16k, 32k,
// 64k, 128k
optional uint64 compression_factor = 7;
}
// Definition has been duplicated from
// $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;
}
+145
View File
@@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2023-2024 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::{Level, debug};
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
#[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),
Lp(lp::LpCli),
Ota(ota::OtaCli),
Payload(payload::PayloadCli),
Sparse(sparse::SparseCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
#[command(hide = true)]
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
#[command(hide = true)]
Extract(ota::ExtractCli),
/// (Deprecated: Use `avbroot boot magisk-info` instead.)
#[command(hide = true)]
MagiskInfo(boot::MagiskInfoCli),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogFormat {
Short,
Medium,
Long,
}
impl Default for LogFormat {
fn default() -> Self {
Self::Short
}
}
impl fmt::Display for LogFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
#[derive(Debug, Parser)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
pub log_format: LogFormat,
}
#[derive(Debug, Clone, Copy)]
pub struct ShortUptime {
epoch: Instant,
}
impl Default for ShortUptime {
fn default() -> Self {
Self {
epoch: Instant::now(),
}
}
}
impl FormatTime for ShortUptime {
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
let e = self.epoch.elapsed();
write!(w, "{:3}.{:03}s", e.as_secs(), e.subsec_millis())
}
}
pub fn init_logging(log_level: Level, log_format: LogFormat) {
let builder = tracing_subscriber::fmt()
.with_writer(io::stderr)
.with_ansi(io::stderr().is_terminal())
.with_max_level(log_level);
match log_format {
LogFormat::Short => {
let format = tracing_subscriber::fmt::format()
.with_timer(ShortUptime::default())
.with_target(false);
builder.event_format(format).init();
}
LogFormat::Medium => {
builder.with_timer(ShortUptime::default()).init();
}
LogFormat::Long => {
builder.pretty().init();
}
}
}
pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Result<()> {
let cli = Cli::parse();
init_logging(cli.log_level, cli.log_format);
logging_initialized.store(true, Ordering::SeqCst);
debug!(?cli);
match cli.command {
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
Command::Boot(c) => boot::boot_main(&c),
Command::Completion(c) => completion::completion_main(&c),
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
Command::Key(c) => key::key_main(&c),
Command::Lp(c) => lp::lp_main(&c, cancel_signal),
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
Command::Payload(c) => payload::payload_main(&c, cancel_signal),
Command::Sparse(c) => sparse::sparse_main(&c, cancel_signal),
// Deprecated aliases.
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
Command::MagiskInfo(c) => boot::magisk_info_subcommand(&c),
}
}
File diff suppressed because it is too large Load Diff
+497
View File
@@ -0,0 +1,497 @@
// 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::{Context, Result, bail};
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,
}
+29
View File
@@ -0,0 +1,29 @@
// 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,
}
+383
View File
@@ -0,0 +1,383 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{self, File},
io::{BufReader, BufWriter, Seek},
path::{Path, PathBuf},
str,
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result, anyhow};
use bstr::ByteSlice;
use cap_std::{ambient_authority, fs::Dir};
use clap::{Parser, Subcommand};
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize};
use crate::{
format::{
compression::{CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
},
stream, util,
};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct CpioInfo {
format: CompressedFormat,
entries: Vec<CpioEntry>,
}
fn open_reader(
path: &Path,
include_trailer: bool,
) -> Result<(
CpioReader<CompressedReader<BufReader<File>>>,
CompressedFormat,
)> {
let file =
File::open(path).with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
let reader = CompressedReader::new(BufReader::new(file), true)
.with_context(|| format!("Failed to open decompressor: {path:?}"))?;
let format = reader.format();
let cpio_reader = CpioReader::new(reader, include_trailer);
Ok((cpio_reader, format))
}
fn open_writer(
path: &Path,
format: CompressedFormat,
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
let file =
File::create(path).with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
let writer = CompressedWriter::new(BufWriter::new(file), format)
.with_context(|| format!("Failed to open compressor: {path:?}"))?;
let cpio_writer = CpioWriter::new(writer, false);
Ok(cpio_writer)
}
fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result<()> {
let compressed_writer = writer.finish().context("Failed to flush cpio writer")?;
let buf_writer = compressed_writer
.finish()
.context("Failed to flush compressor")?;
buf_writer.into_inner().context("Failed to flush file")?;
Ok(())
}
/// Read cpio information from TOML file.
fn read_info(path: &Path) -> Result<CpioInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read cpio info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse cpio info TOML: {path:?}"))?;
Ok(info)
}
/// Write cpio information to TOML file.
fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize cpio info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write cpio info TOML: {path:?}"))?;
Ok(())
}
/// Open reader to the corresponding file inside the tree if the entry is a
/// regular file. Unsafe paths will result in an error.
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let mut reader = tree
.open(path)
.map(|f| BufReader::new(f.into_std()))
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let file_size = reader
.seek(std::io::SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {path:?}"))?
.to_u32()
.ok_or_else(|| anyhow!("File is too large: {path:?}"))?;
reader
.rewind()
.with_context(|| format!("Failed to seek file: {path:?}"))?;
Ok(Some((reader, file_size)))
} else {
Ok(None)
}
}
/// Open writer to the corresponding file inside the tree if the entry is a
/// regular file. Intermediate directories are automatically created as needed.
/// Unsafe paths will result in an error.
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let parent = util::parent_path(path);
tree.create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
let writer = tree
.create(path)
.map(|f| BufWriter::new(f.into_std()))
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
Ok(Some(writer))
} else {
Ok(None)
}
}
fn display_format(cli: &CpioCli, format: CompressedFormat) {
if !cli.quiet {
println!("Compression format: {format:?}");
}
}
fn display_entry(cli: &CpioCli, entry: &CpioEntry) {
if !cli.quiet {
println!();
println!("{entry}");
}
}
fn unpack_subcommand(
cpio_cli: &CpioCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, false)?;
let mut info = CpioInfo {
format,
entries: vec![],
};
display_format(cpio_cli, format);
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_tree, authority)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
let file_size = entry.data.size()?;
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
writer.into_inner().context("Failed to flush data")?;
}
info.entries.push(entry);
}
write_info(&cli.output_info, &info)?;
Ok(())
}
fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut info = read_info(&cli.input_info)?;
let mut writer = open_writer(&cli.output, info.format)?;
display_format(cpio_cli, info.format);
if cli.sort {
cpio::sort(&mut info.entries);
}
cpio::assign_inodes(&mut info.entries, true)?;
let authority = ambient_authority();
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
for entry in &mut info.entries {
let out = open_tree_file(&tree, entry)?;
if let Some((_, file_size)) = &out {
entry.data = CpioEntryData::Size(*file_size);
}
display_entry(cpio_cli, entry);
writer
.start_entry(entry)
.context("Failed to write cpio entry")?;
if let Some((mut reader, file_size)) = out {
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
}
}
flush_writer(writer)?;
Ok(())
}
fn repack_subcommand(
cpio_cli: &CpioCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, false)?;
let mut writer = open_writer(&cli.output, format)?;
display_format(cpio_cli, format);
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
writer
.start_entry(&entry)
.context("Failed to write cpio entry")?;
if let CpioEntryData::Size(s) = &entry.data {
stream::copy_n(&mut reader, &mut writer, u64::from(*s), cancel_signal)
.context("Failed to copy cpio entry data")?;
}
}
flush_writer(writer)?;
Ok(())
}
fn info_subcommand(cpio_cli: &CpioCli, cli: &InfoCli) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, cli.trailer)?;
display_format(cpio_cli, format);
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
}
Ok(())
}
pub fn cpio_main(cli: &CpioCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
CpioCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
CpioCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
CpioCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
CpioCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack a cpio archive.
///
/// Regular files will be extracted to the output tree directory, but not any
/// other type of file (eg. symlinks). All file metadata is written to the info
/// TOML file, like the UID/GID, permissions, and symlink targets.
///
/// If any paths inside the cpio archive are unsafe, the extraction process will
/// fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
output_info: PathBuf,
/// Path to output files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
output_tree: PathBuf,
}
/// Pack a cpio archive.
///
/// The new cpio archive will *only* contain files listed in the info TOML file.
/// Extra files inside the input tree directory that aren't listed will be
/// silently ignored. Entries are added to the archive in the order that they
/// are listed unless --sort is specified.
///
/// All fields inside the info TOML are used as-is. Missing fields in entries
/// are set to 0, aside from the inode number, which will be assigned a unique
/// value.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
input_info: PathBuf,
/// Path to input files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
input_tree: PathBuf,
/// Sort entries before packing.
#[arg(long)]
sort: bool,
}
/// Repack a cpio archive.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
/// Display cpio entry information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Show cpio trailer entry.
#[arg(long, global = true)]
trailer: bool,
}
#[derive(Debug, Subcommand)]
enum CpioCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect cpio archives.
#[derive(Debug, Parser)]
pub struct CpioCli {
#[command(subcommand)]
command: CpioCommand,
/// Don't print cpio entry information.
#[arg(short, long, global = true)]
quiet: bool,
}
+189
View File
@@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{File, OpenOptions},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::fec::FecImage,
stream::{FromReader, PSeekFile, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
fn read_fec(path: &Path) -> Result<FecImage> {
let reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let fec = FecImage::from_reader(reader)
.with_context(|| format!("Failed to read FEC data: {path:?}"))?;
Ok(fec)
}
fn write_fec(path: &Path, fec: &FecImage) -> Result<()> {
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
fec.to_writer(&mut writer)
.with_context(|| format!("Failed to write FEC data: {path:?}"))?;
writer
.flush()
.with_context(|| format!("Failed to flush FEC data: {path:?}"))?;
Ok(())
}
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let fec = FecImage::generate(&input, cli.parity, cancel_signal)
.context("Failed to generate FEC data")?;
write_fec(&cli.fec, &fec)?;
Ok(())
}
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
let ranges = cli
.range
.chunks_exact(2)
.map(|w| w[0]..w[1])
.collect::<Vec<_>>();
let input = open_input(&cli.input, false)?;
let mut fec = read_fec(&cli.fec)?;
fec.update(&input, &ranges, cancel_signal)
.context("Failed to update FEC data")?;
write_fec(&cli.fec, &fec)?;
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let fec = read_fec(&cli.fec)?;
fec.verify(&input, cancel_signal)
.context("Failed to verify data")?;
Ok(())
}
fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, true)?;
let fec = read_fec(&cli.fec)?;
// The separate buffered readers and writers are safe because the function
// guarantees that every thread touches disjoint offsets and every offset is
// read and written at most once.
fec.repair(&input, &input, cancel_signal)
.context("Failed to repair file")?;
Ok(())
}
pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
FecCommand::Generate(c) => generate_subcommand(c, cancel_signal),
FecCommand::Update(c) => update_subcommand(c, cancel_signal),
FecCommand::Verify(c) => verify_subcommand(c, cancel_signal),
FecCommand::Repair(c) => repair_subcommand(c, cancel_signal),
}
}
/// Generate FEC data for a file.
#[derive(Debug, Parser)]
struct GenerateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output FEC data.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
/// Number of parity bytes per RS block (min 2, max 24).
#[arg(short, long, value_name = "BYTES", default_value = "2")]
parity: u8,
}
/// Update FEC data after a file is modified.
#[derive(Debug, Parser)]
struct UpdateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to FEC data.
///
/// The file will be modified in place.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
/// Input file ranges that were updated.
///
/// This is a half-open range and can be specified multiple times.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
range: Vec<u64>,
}
/// Verify that a file contains no errors.
#[derive(Debug, Parser)]
struct VerifyCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input FEC data.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
}
/// Repair a file.
#[derive(Debug, Parser)]
struct RepairCli {
/// Path to data.
///
/// The file will be modified in place.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input FEC data.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
}
#[derive(Debug, Subcommand)]
enum FecCommand {
Generate(GenerateCli),
Update(UpdateCli),
Verify(VerifyCli),
Repair(RepairCli),
}
/// Generate dm-verity FEC data and verify/repair files.
///
/// These commands operate on FEC files with AOSP's header format.
#[derive(Debug, Parser)]
pub struct FecCli {
#[command(subcommand)]
command: FecCommand,
}
+174
View File
@@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{File, OpenOptions},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::hashtree::HashTreeImage,
stream::{FromReader, PSeekFile, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
fn read_hash_tree(path: &Path) -> Result<HashTreeImage> {
let reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let hash_tree = HashTreeImage::from_reader(reader)
.with_context(|| format!("Failed to read hash tree data: {path:?}"))?;
Ok(hash_tree)
}
fn write_hash_tree(path: &Path, hash_tree: &HashTreeImage) -> Result<()> {
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
hash_tree
.to_writer(&mut writer)
.with_context(|| format!("Failed to write hash tree data: {path:?}"))?;
writer
.flush()
.with_context(|| format!("Failed to flush hash tree data: {path:?}"))?;
Ok(())
}
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
let salt = hex::decode(&cli.salt).context("Invalid salt")?;
let input = open_input(&cli.input, false)?;
let hash_tree =
HashTreeImage::generate(&input, cli.block_size, &cli.algorithm, &salt, cancel_signal)
.context("Failed to generate hash tree data")?;
write_hash_tree(&cli.hash_tree, &hash_tree)?;
Ok(())
}
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
let ranges = cli
.range
.chunks_exact(2)
.map(|w| w[0]..w[1])
.collect::<Vec<_>>();
let input = open_input(&cli.input, false)?;
let mut hash_tree = read_hash_tree(&cli.hash_tree)?;
hash_tree
.update(&input, &ranges, cancel_signal)
.context("Failed to update hash tree data")?;
write_hash_tree(&cli.hash_tree, &hash_tree)?;
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let hash_tree = read_hash_tree(&cli.hash_tree)?;
hash_tree
.verify(&input, cancel_signal)
.context("Failed to verify data")?;
Ok(())
}
pub fn hash_tree_main(cli: &HashTreeCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
HashTreeCommand::Generate(c) => generate_subcommand(c, cancel_signal),
HashTreeCommand::Update(c) => update_subcommand(c, cancel_signal),
HashTreeCommand::Verify(c) => verify_subcommand(c, cancel_signal),
}
}
/// Generate hash tree data for a file.
#[derive(Debug, Parser)]
struct GenerateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output hash tree data.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
/// Block size.
#[arg(short, long, value_name = "BYTES", default_value = "4096")]
block_size: u32,
/// Hash algorithm.
#[arg(short, long, value_name = "NAME", default_value = "sha256")]
algorithm: String,
/// Salt (in hex).
#[arg(short, long, value_name = "HEX", default_value = "")]
salt: String,
}
/// Update hash tree data after a file is modified.
#[derive(Debug, Parser)]
struct UpdateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to hash tree data.
///
/// The file will be modified in place.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
/// Input file ranges that were updated.
///
/// This is a half-open range and can be specified multiple times.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
range: Vec<u64>,
}
/// Verify that a file contains no errors.
#[derive(Debug, Parser)]
struct VerifyCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input hash tree data.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
}
#[derive(Debug, Subcommand)]
enum HashTreeCommand {
Generate(GenerateCli),
Update(UpdateCli),
Verify(VerifyCli),
}
/// Generate dm-verity hash tree data and verify files.
///
/// These commands operate on a standard hash tree data prepended by a custom
/// header.
#[derive(Debug, Parser)]
pub struct HashTreeCli {
#[command(subcommand)]
command: HashTreeCommand,
}
+198
View File
@@ -0,0 +1,198 @@
// 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) | KeyCommand::EncodeAvb(c) => {
let public_key = if let Some(p) = &c.input.key {
let passphrase = get_passphrase_source(&c.passphrase, p);
let private_key = crypto::read_pem_key_file(p, &passphrase)
.with_context(|| format!("Failed to load key: {p:?}"))?;
private_key.to_public_key()
} else if let Some(p) = &c.input.public_key {
crypto::read_pem_public_key_file(p)
.with_context(|| format!("Failed to load public key: {p:?}"))?
} else if let Some(p) = &c.input.cert {
let certificate = crypto::read_pem_cert_file(p)
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
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 public key.
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
public_key: Option<PathBuf>,
/// Path to certificate.
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
cert: Option<PathBuf>,
}
#[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,
}
/// Convert a key or certificate to an AVB-encoded public key.
///
/// The public key is stored in both the private key and the certificate. Either
/// one can be used interchangeably.
#[derive(Debug, Parser)]
struct EncodeAvbCli {
/// 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),
/// (Deprecated: Use `avbroot key encode-avb` instead.)
#[command(hide = true)]
ExtractAvb(EncodeAvbCli),
EncodeAvb(EncodeAvbCli),
DecodeAvb(DecodeAvbCli),
}
/// Generate and convert keys.
#[derive(Debug, Parser)]
pub struct KeyCli {
#[command(subcommand)]
command: KeyCommand,
}
+676
View File
@@ -0,0 +1,676 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
ffi::OsStr,
fs::{self, File},
io::{Seek, SeekFrom},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result, bail};
use cap_std::{ambient_authority, fs::Dir};
use clap::{CommandFactory, Parser, Subcommand};
use rayon::iter::{
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator,
};
use crate::{
format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE},
stream::{self, FromReader, PSeekFile, Reopen, ToWriter},
};
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<PSeekFile>, Metadata)> {
let mut inputs = paths
.iter()
.map(|p| {
let p = p.as_ref();
File::open(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
})
.collect::<Result<Vec<_>>>()?;
let metadata = Metadata::from_reader(&mut inputs[0])
.with_context(|| format!("Failed to parse LP image metadata: {:?}", paths[0].as_ref()))?;
Ok((inputs, metadata))
}
fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<PSeekFile>> {
paths
.iter()
.map(|p| {
let p = p.as_ref();
File::create(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for writing: {p:?}"))
})
.collect::<Result<Vec<_>>>()
}
fn read_info(path: &Path) -> Result<Metadata> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read metadata info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse metadata info TOML: {path:?}"))?;
Ok(info)
}
fn write_info(path: &Path, metadata: &Metadata) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(metadata)
.with_context(|| format!("Failed to serialize metadata info TOML: {path:?}"))?;
fs::write(path, data)
.with_context(|| format!("Failed to write metadata info TOML: {path:?}"))?;
Ok(())
}
fn display_metadata(cli: &LpCli, metadata: &Metadata) {
if !cli.quiet {
println!("{metadata:#?}");
}
}
struct CopyExtent {
device_index: usize,
lp_offset: u64,
out_offset: u64,
size: u64,
}
/// Split extents into smaller ones for parallelization.
fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
// 64 MiB is the smallest size we'll parallelize by.
const CHUNK_SIZE: u64 = 64 * 1024 * 1024;
let mut result = vec![];
let mut out_offset = 0;
for extent in extents {
let mut remain = extent.num_sectors * u64::from(SECTOR_SIZE);
match extent.extent_type {
ExtentType::Linear {
start_sector,
block_device_index,
} => {
let mut lp_offset = start_sector * u64::from(SECTOR_SIZE);
// 64 MiB is the smallest size we'll parallelize by.
let num_chunks = remain.div_ceil(64 * 1024 * 1024);
for _ in 0..num_chunks {
let chunk_size = CHUNK_SIZE.min(remain);
result.push(CopyExtent {
device_index: block_device_index,
out_offset,
lp_offset,
size: chunk_size,
});
out_offset += chunk_size;
lp_offset += chunk_size;
remain -= chunk_size;
}
}
ExtentType::Zero => out_offset += remain,
}
}
result
}
/// Use the CLI-specified slot or automatically select one if all slots are
/// identical.
fn get_slot_number(metadata: &Metadata, cli_slot: Option<u32>) -> Result<usize> {
if let Some(n) = cli_slot {
let n = n as usize;
if n >= metadata.slots.len() {
bail!("Slot out of range: {n}");
}
Ok(n)
} else {
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
bail!("A slot must be specified because they are not all identical");
}
Ok(0)
}
}
/// Remove all slots aside from the specified one and return the old slot count.
fn retain_slot(metadata: &mut Metadata, slot: usize) -> usize {
let slot_count = metadata.slots.len();
metadata.slots.swap(0, slot);
metadata.slots.truncate(1);
slot_count
}
/// Duplicate the first slot until the required number of slots is reached.
fn fill_slots(metadata: &mut Metadata) {
let required = match metadata.image_type {
ImageType::Normal => metadata.metadata_slot_count as usize,
ImageType::Empty => 1,
};
for _ in metadata.slots.len()..required {
metadata.slots.extend_from_within(0..=0);
}
}
fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut inputs = cli
.input
.iter()
.map(|p| {
File::open(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
})
.collect::<Result<Vec<_>>>()?;
let mut metadata = Metadata::from_reader(&mut inputs[0])
.with_context(|| format!("Failed to read LP image metadata: {:?}", cli.input[0]))?;
// Display and write only the selected slot.
let slot_number = get_slot_number(&metadata, cli.slot)?;
retain_slot(&mut metadata, slot_number);
display_metadata(lp_cli, &metadata);
write_info(&cli.output_info, &metadata)?;
// For empty images, there's no data to unpack.
if metadata.image_type == ImageType::Empty {
return Ok(());
}
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_images, authority)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
let slot = &metadata.slots[0];
if slot.block_devices.len() != inputs.len() {
bail!(
"Need {} input images, but have {}",
slot.block_devices.len(),
inputs.len(),
);
}
// Preopen all image output files.
let mut paths = vec![];
let mut files = vec![];
for group in &slot.groups {
let mut group_paths = vec![];
let mut group_files = vec![];
for partition in &group.partitions {
// A partition name with unsafe characters fails during parsing.
let path = format!("{}.img", partition.name);
let file = directory
.create(&path)
.map(|f| PSeekFile::new(f.into_std()))
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
file.set_len(partition.size()?)
.with_context(|| format!("Failed to truncate file: {path:?}"))?;
group_paths.push(path);
group_files.push(file);
}
paths.push(group_paths);
files.push(group_files);
}
slot.groups
.par_iter()
.enumerate()
// Flatten grouped partitions.
.flat_map(|(g_index, g)| {
g.partitions
.par_iter()
.enumerate()
.map(move |(p_index, p)| (g_index, p_index, p))
})
// Flatten extents in all partitions and split them to smaller chunks
// for better parallelism.
.flat_map(|(g_index, p_index, p)| {
split_extents(&p.extents)
.into_par_iter()
.map(move |e| (g_index, p_index, e))
})
.map(|(g_index, p_index, extent)| {
// Never fails for PSeekFiles.
let mut reader = inputs[extent.device_index].reopen()?;
let mut writer = files[g_index][p_index].reopen()?;
let r_path = &cli.input[extent.device_index];
let w_path = &paths[g_index][p_index];
reader
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
writer
.seek(SeekFrom::Start(extent.out_offset))
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut metadata = read_info(&cli.input_info)?;
if metadata.slots.len() != 1 {
bail!("There must be exactly one metadata slot");
}
let slot = &mut metadata.slots[0];
let mut outputs = open_lp_outputs(&cli.output)?;
if slot.block_devices.len() != outputs.len() {
bail!(
"Need {} output images, but have {}",
slot.block_devices.len(),
outputs.len(),
);
}
if metadata.image_type == ImageType::Normal {
for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() {
output
.set_len(block_device.size)
.with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?;
}
}
for group in &slot.groups {
for partition in &group.partitions {
let name = &partition.name;
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
}
// Preopen all image input files.
let mut paths = vec![];
let mut files = vec![];
if metadata.image_type == ImageType::Normal {
let authority = ambient_authority();
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
for group in &mut slot.groups {
let mut group_paths = vec![];
let mut group_files = vec![];
for partition in &mut group.partitions {
let path = format!("{}.img", partition.name);
let mut file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let size = file
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to seek file: {path:?}"))?;
if size % u64::from(SECTOR_SIZE) != 0 {
bail!("File size is not {SECTOR_SIZE}B aligned: {size}: {path:?}");
}
// This will be filled out properly later during reallocation.
partition.extents.push(Extent {
num_sectors: size / u64::from(SECTOR_SIZE),
extent_type: ExtentType::Linear {
start_sector: 0,
block_device_index: 0,
},
});
group_paths.push(path);
group_files.push(file);
}
paths.push(group_paths);
files.push(group_files);
}
// Now that we have all the partition sizes, actually allocate extents
// for them on the block devices.
slot.reallocate_extents()
.context("Failed to allocate extents")?;
}
// Display only the selected slot and make the rest identical.
let _ = slot;
display_metadata(lp_cli, &metadata);
fill_slots(&mut metadata);
let slot = &metadata.slots[0];
// Write the new metadata.
metadata
.to_writer(&mut outputs[0])
.with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?;
// For empty images, there's no data to pack.
if metadata.image_type == ImageType::Empty {
return Ok(());
}
slot.groups
.par_iter()
.enumerate()
// Flatten grouped partitions.
.flat_map(|(g_index, g)| {
g.partitions
.par_iter()
.enumerate()
.map(move |(p_index, p)| (g_index, p_index, p))
})
// Flatten extents in all partitions and split them to smaller chunks
// for better parallelism.
.flat_map(|(g_index, p_index, p)| {
split_extents(&p.extents)
.into_par_iter()
.map(move |e| (g_index, p_index, e))
})
.map(|(g_index, p_index, extent)| {
// Never fails for PSeekFiles.
let mut reader = files[g_index][p_index].reopen()?;
let mut writer = outputs[extent.device_index].reopen()?;
let r_path = &paths[g_index][p_index];
let w_path = &cli.output[extent.device_index];
reader
.seek(SeekFrom::Start(extent.out_offset))
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
writer
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
// Show a clap-style error if the number of inputs and outputs aren't equal.
if cli.input.len() != cli.output.len() {
let (arg_id, actual_len, expected_len) = if cli.input.len() < cli.output.len() {
("input", cli.input.len(), cli.output.len())
} else {
("output", cli.output.len(), cli.input.len())
};
let mut command = RepackCli::command();
command.build();
let arg = command
.get_arguments()
.find(|a| a.get_id() == arg_id)
.expect("argument not found");
let mut error =
clap::Error::new(clap::error::ErrorKind::WrongNumberOfValues).with_cmd(&command);
error.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
error.insert(
clap::error::ContextKind::ActualNumValues,
clap::error::ContextValue::Number(actual_len as isize),
);
error.insert(
clap::error::ContextKind::ExpectedNumValues,
clap::error::ContextValue::Number(expected_len as isize),
);
// We don't show the usage because only Command::_build_subcommand() can
// create an appropriate Command instance for showing the subcommand
// usage and there's no way to call that, directly or indirectly.
error.exit();
}
let (inputs, mut metadata) = open_lp_inputs(&cli.input)?;
let mut outputs = open_lp_outputs(&cli.output)?;
// Display only the selected slot and make the rest identical.
let slot_number = get_slot_number(&metadata, cli.slot)?;
retain_slot(&mut metadata, slot_number);
display_metadata(lp_cli, &metadata);
fill_slots(&mut metadata);
let slot = &metadata.slots[0];
if slot.block_devices.len() != inputs.len() {
bail!(
"Need {} images, but have {}",
slot.block_devices.len(),
inputs.len(),
);
}
// Write the new metadata.
metadata
.to_writer(&mut outputs[0])
.with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?;
// Explicitly set the file size in case there are dm-zero extents, which are
// ignored below.
if metadata.image_type == ImageType::Normal {
for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() {
output
.set_len(block_device.size)
.with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?;
}
}
slot.groups
.par_iter()
// Flatten grouped partitions.
.flat_map(|group| &group.partitions)
// Flatten extents in all partitions and split them to smaller chunks
// for better parallelism.
.flat_map(|partition| split_extents(&partition.extents))
.map(|extent| {
// Never fails for PSeekFiles.
let mut reader = inputs[extent.device_index].reopen()?;
let mut writer = outputs[extent.device_index].reopen()?;
let r_path = &cli.input[extent.device_index];
let w_path = &cli.output[extent.device_index];
reader
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
writer
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn info_subcommand(lp_cli: &LpCli, cli: &InfoCli) -> Result<()> {
let (_, metadata) = open_lp_inputs(&[&cli.input])?;
// Unlike the other subcommands, we show all metadata slots here.
display_metadata(lp_cli, &metadata);
Ok(())
}
pub fn lp_main(cli: &LpCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
LpCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
LpCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
LpCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
LpCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack an LP image.
///
/// The LP image metadata is written to the info TOML file. For normal images,
/// each partition is extracted to `<partition name>.img` in the output images
/// directory. For empty images, the output images directory is unused.
///
/// If any partition names are unsafe to use in a path, the extraction process
/// will fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input LP images.
///
/// If there are multiple images, they must be specified in order. If the
/// order is unknown, run `avbroot lp info` against the `super` image and
/// look at the `block_devices` field.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
input: Vec<PathBuf>,
/// Path to output info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")]
output_info: PathBuf,
/// Path to output images directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
output_images: PathBuf,
/// The LP metadata slot to use.
///
/// This slot is the only slot where data extents are copied from. Any data
/// referenced exclusively by other slots (if any) will be ignored.
///
/// This option is required if not all slots are identical.
#[arg(short, long)]
slot: Option<u32>,
}
/// Pack an LP image.
///
/// For normal images, the number of metadata slots written is equal to the
/// `metadata_slot_count` value in the info TOML. Each slot has identical
/// metadata. It is not possible to write multiple slots with different metadata
/// using this tool. For empty images, only a single slot is written, regardless
/// of the value of `metadata_slot_count`, as required by the file format.
///
/// The new LP image will *only* contain images listed in the info TOML file and
/// they are added in the order listed. The input images directory is not used
/// when packing an empty image.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output LP images.
///
/// If there are multiple images, they must be specified in the same order
/// as the block device entries are listed in the info TOML.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
output: Vec<PathBuf>,
/// Path to input info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")]
input_info: PathBuf,
/// Path to input images directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
input_images: PathBuf,
}
/// Repack an LP image.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input LP images.
///
/// If there are multiple images, they must be specified in order. If the
/// order is unknown, run `avbroot lp info` against the `super` image and
/// look at the `block_devices` field.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
input: Vec<PathBuf>,
/// Path to output LP images.
///
/// The number of output images must equal the number of input images.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
output: Vec<PathBuf>,
/// The LP metadata slot to use.
///
/// This slot is the only slot where data extents are copied to the output
/// images. Any data referenced exclusively by other slots (if any) will be
/// ignored.
///
/// This option is required if not all slots are identical.
#[arg(short, long)]
slot: Option<u32>,
}
/// Display LP image metadata.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input LP image.
///
/// If there are multiple images, this should refer to the first one, which
/// is usually the `super` image. The other images are not needed when
/// inspecting the metadata because the metadata is only stored in the first
/// image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum LpCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect LP images.
#[derive(Debug, Parser)]
pub struct LpCli {
#[command(subcommand)]
command: LpCommand,
/// Don't print LP metadata information.
#[arg(short, long, global = true)]
quiet: bool,
}
+15
View File
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod args;
pub mod avb;
pub mod boot;
pub mod completion;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod key;
pub mod lp;
pub mod ota;
pub mod payload;
pub mod sparse;
File diff suppressed because it is too large Load Diff
+471
View File
@@ -0,0 +1,471 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
fs::{self, File},
io::{BufReader, BufWriter, Seek, SeekFrom},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result, anyhow, bail};
use cap_std::{ambient_authority, fs::Dir};
use clap::{Args, Parser, Subcommand};
use tracing::info;
use crate::{
cli::ota,
crypto::{self, PassphraseSource, RsaSigningKey},
format::payload::{PayloadHeader, PayloadWriter},
stream::{self, FromReader, PSeekFile},
};
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
let mut reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open payload for reading: {path:?}"))?;
let header = PayloadHeader::from_reader(&mut reader)
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
if !allow_delta && !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
Ok((reader, header))
}
fn open_writer(
path: &Path,
header: PayloadHeader,
key: RsaSigningKey,
) -> Result<PayloadWriter<BufWriter<File>>> {
let writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open payload for writing: {path:?}"))?;
let payload_writer = PayloadWriter::new(writer, header, key)
.with_context(|| format!("Failed to write payload header: {path:?}"))?;
Ok(payload_writer)
}
fn read_info(path: &Path) -> Result<PayloadHeader> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read payload info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse payload info TOML: {path:?}"))?;
Ok(info)
}
fn write_info(path: &Path, manifest: &PayloadHeader) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(manifest)
.with_context(|| format!("Failed to serialize payload info TOML: {path:?}"))?;
fs::write(path, data)
.with_context(|| format!("Failed to write payload info TOML: {path:?}"))?;
Ok(())
}
fn display_header(cli: &PayloadCli, header: &PayloadHeader) {
if !cli.quiet {
println!("{header:#?}");
}
}
fn load_key(group: &KeyGroup) -> Result<RsaSigningKey> {
let source = PassphraseSource::new(
&group.key,
group.pass_file.as_deref(),
group.pass_env_var.as_deref(),
);
let signing_key = if let Some(helper) = &group.signing_helper {
let public_key = crypto::read_pem_public_key_file(&group.key)
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
RsaSigningKey::External {
program: helper.clone(),
public_key_file: group.key.clone(),
public_key,
passphrase_source: source,
}
} else {
let private_key = crypto::read_pem_key_file(&group.key, &source)
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
RsaSigningKey::Internal(private_key)
};
Ok(signing_key)
}
fn unpack_subcommand(
payload_cli: &PayloadCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, header) = open_reader(&cli.input, false)?;
let payload_size = reader
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
display_header(payload_cli, &header);
write_info(&cli.output_info, &header)?;
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_images, authority)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
ota::extract_payload(
&PSeekFile::new(reader.into_inner()),
&directory,
0,
payload_size,
&header,
&header
.manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.cloned()
.collect(),
cancel_signal,
)?;
Ok(())
}
fn pack_subcommand(
payload_cli: &PayloadCli,
cli: &PackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let mut header = read_info(&cli.input_info)?;
let authority = ambient_authority();
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
for p in &header.manifest.partitions {
let name = &p.partition_name;
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
// Pre-open all of the image files.
let input_files = header
.manifest
.partitions
.iter()
.map(|p| {
let path = format!("{}.img", p.partition_name);
let file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
.with_context(|| format!("Failed to open file: {path:?}"))?;
Ok((p.partition_name.clone(), file))
})
.collect::<Result<HashMap<_, _>>>()?;
// Compress the images and compute the list of install operations for
// insertion into the payload header. The compressed data is stored in new
// temp files and the original input files are dropped.
let mut compressed_files = input_files
.into_iter()
.map(|(name, mut input_file)| {
ota::compress_image(&name, &mut input_file, &mut header, None, cancel_signal)
.with_context(|| format!("Failed to compress image: {name}"))?;
Ok((name, input_file))
})
.collect::<Result<HashMap<_, _>>>()?;
info!("Generating new OTA payload");
// Now we can write the actual payload. With everything precomputed, this is
// mostly just a simple copy.
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
while payload_writer
.begin_next_operation()
.context("Failed to begin next payload blob entry")?
{
let name = payload_writer.partition().unwrap().partition_name.clone();
let operation = payload_writer.operation().unwrap();
let Some(data_length) = operation.data_length else {
// Otherwise, this is a ZERO/DISCARD operation.
continue;
};
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
.ok_or_else(|| anyhow!("Missing data_offset in partition #{pi} operation #{oi}"))?;
// The compressed chunks are laid out sequentially and data_offset is
// set to the offset within that file.
let Some(input_file) = compressed_files.get_mut(&name) else {
unreachable!("Compressed data not found for image: {name}");
};
input_file
.seek(SeekFrom::Start(data_offset))
.with_context(|| format!("Failed to seek image: {name}"))?;
stream::copy_n(input_file, &mut payload_writer, data_length, cancel_signal)
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
}
let (_, header, properties, _) = payload_writer
.finish()
.context("Failed to finalize payload")?;
// Display the header information now that it has been finalized.
display_header(payload_cli, &header);
// Optionally, write payload_properties.txt.
if let Some(path) = &cli.output_properties {
fs::write(path, properties)
.with_context(|| format!("Failed to write payload properties: {path:?}"))?;
}
Ok(())
}
fn repack_subcommand(
payload_cli: &PayloadCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let (mut reader, header) = open_reader(&cli.input, true)?;
info!("Generating new OTA payload");
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
while payload_writer
.begin_next_operation()
.context("Failed to begin next payload blob entry")?
{
let name = payload_writer.partition().unwrap().partition_name.clone();
let operation = payload_writer.operation().unwrap();
let Some(data_length) = operation.data_length else {
// Otherwise, this is a ZERO/DISCARD operation.
continue;
};
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
.ok_or_else(|| anyhow!("Missing data_offset in partition #{pi} operation #{oi}"))?;
// Directly copy blobs from the original payload.
let data_offset = data_offset
.checked_add(header.blob_offset)
.ok_or_else(|| anyhow!("data_offset overflow in partition #{pi} operation #{oi}"))?;
reader
.seek(SeekFrom::Start(data_offset))
.with_context(|| format!("Failed to seek original payload to {data_offset}"))?;
stream::copy_n(&mut reader, &mut payload_writer, data_length, cancel_signal)
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
}
let (_, header, properties, _) = payload_writer
.finish()
.context("Failed to finalize payload")?;
// Display the header information now that it has been finalized.
display_header(payload_cli, &header);
// Optionally, write payload_properties.txt.
if let Some(path) = &cli.output_properties {
fs::write(path, properties)
.with_context(|| format!("Failed to write payload properties: {path:?}"))?;
}
Ok(())
}
fn info_subcommand(payload_cli: &PayloadCli, cli: &InfoCli) -> Result<()> {
let (_, header) = open_reader(&cli.input, true)?;
display_header(payload_cli, &header);
Ok(())
}
pub fn payload_main(cli: &PayloadCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
PayloadCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
PayloadCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
PayloadCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
PayloadCommand::Info(c) => info_subcommand(cli, c),
}
}
#[derive(Debug, Args)]
struct KeyGroup {
/// Path to signing key.
///
/// This should normally be a private key. However, if --signing-helper is
/// used, then it should be a public key instead.
#[arg(short, long, value_name = "FILE", value_parser)]
key: PathBuf,
/// Environment variable containing private key passphrase.
#[arg(long, value_name = "ENV_VAR", value_parser, group = "pass")]
pass_env_var: Option<OsString>,
/// File containing private key passphrase.
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
pass_file: Option<PathBuf>,
/// External program for signing.
///
/// If this option is specified, then --key must refer to a public key. The
/// program will be invoked as:
///
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
#[arg(long, value_name = "PROGRAM", value_parser)]
signing_helper: Option<PathBuf>,
}
/// Unpack a payload binary.
///
/// Each partition is extracted to `<partition name>.img` in the output images
/// directory. The payload header metadata is written to the info TOML file.
///
/// If any partition names are unsafe to use in a path, the extraction process
/// will fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output info TOML.
#[arg(
long,
value_name = "FILE",
value_parser,
default_value = "payload.toml"
)]
output_info: PathBuf,
/// Path to output images directory.
#[arg(
long,
value_name = "DIR",
value_parser,
default_value = "payload_images"
)]
output_images: PathBuf,
}
/// Pack a payload binary.
///
/// The new payload binary will *only* contain images listed in the info TOML
/// file. Extra images in the input images directory that aren't listed will be
/// silently ignored. Images are added to the payload in the order that they are
/// listed in the info TOML file.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to output payload properties file.
#[arg(short, long, value_name = "FILE", value_parser)]
output_properties: Option<PathBuf>,
/// Path to input info TOML.
#[arg(
long,
value_name = "FILE",
value_parser,
default_value = "payload.toml"
)]
input_info: PathBuf,
/// Path to input images directory.
#[arg(
long,
value_name = "DIR",
value_parser,
default_value = "payload_images"
)]
input_images: PathBuf,
#[command(flatten)]
key: KeyGroup,
}
/// Repack a payload binary.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk nor recompressing the partition images.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to output payload properties file.
#[arg(short, long, value_name = "FILE", value_parser)]
output_properties: Option<PathBuf>,
#[command(flatten)]
key: KeyGroup,
}
/// Display payload information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input payload file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum PayloadCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect OTA payloads.
#[derive(Debug, Parser)]
pub struct PayloadCli {
#[command(subcommand)]
command: PayloadCommand,
/// Don't print payload header information.
#[arg(short, long, global = true)]
quiet: bool,
}
+631
View File
@@ -0,0 +1,631 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
ops::Range,
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Parser, Subcommand};
use crc32fast::Hasher;
use zerocopy::{IntoBytes, little_endian};
use crate::{
format::{
padding,
sparse::{
self, Chunk, ChunkBounds, ChunkData, ChunkList, CrcMode, Header, SparseReader,
SparseWriter,
},
},
stream,
};
struct CompactView<'a, T>(&'a [T]);
impl<T: fmt::Debug> fmt::Debug for CompactView<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
for item in self.0 {
// No alternate mode for no inner newlines.
list.entry(&format_args!("{item:?}"));
}
list.finish()
}
}
#[derive(Clone)]
struct Metadata {
header: Header,
chunks: Vec<Chunk>,
}
impl fmt::Debug for Metadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Metadata")
.field("header", &self.header)
.field("chunks", &CompactView(&self.chunks))
.finish()
}
}
fn open_reader(path: &Path) -> Result<File> {
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))
}
fn open_writer(path: &Path, truncate: bool) -> Result<File> {
OpenOptions::new()
.write(true)
.create(true)
.truncate(truncate)
.open(path)
.with_context(|| format!("Failed to open for writing: {path:?}"))
}
fn display_metadata(cli: &SparseCli, metadata: &Metadata) {
if !cli.quiet {
println!("{metadata:#?}");
}
}
/// Splits large data chunks to ensure that none exceed 64 MiB. This is not
/// necessary in most cases, but is kept to match the behavior of AOSP's
/// libsparse.
fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
const MAX_BYTES: u32 = 64 * 1024 * 1024;
let max_blocks_per_chunk = MAX_BYTES / block_size;
let mut result = vec![];
for mut chunk in chunks.iter().copied() {
if chunk.data == ChunkData::Data {
while chunk.bounds.len() > max_blocks_per_chunk {
result.push(Chunk {
bounds: ChunkBounds {
start: chunk.bounds.start,
end: chunk.bounds.start + max_blocks_per_chunk,
},
data: chunk.data,
});
chunk.bounds.start += max_blocks_per_chunk;
}
}
result.push(chunk);
}
result
}
/// [Linux only] Find allocated regions of the file. This avoids needing to read
/// unused portions of the file if it is a native sparse file.
#[cfg(any(target_os = "linux", target_os = "android"))]
fn find_allocated_regions(
path: &Path,
reader: &File,
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<u64>>> {
use rustix::{fs::SeekFrom, io::Errno};
let mut result = vec![];
let mut start;
let mut end = 0;
loop {
stream::check_cancel(cancel_signal)?;
start = match rustix::fs::seek(reader, SeekFrom::Data(end)) {
Ok(offset) => offset,
Err(e) if e == Errno::NXIO => break,
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
};
end = rustix::fs::seek(reader, SeekFrom::Hole(start))
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
result.push(start..end);
}
Ok(result)
}
/// Compute chunk boundaries for the list of potentially overlapping file byte
/// regions. If `exact_bounds` is true, then the regions must be block-aligned.
/// Otherwise, the lower boundaries are aligned down and the upper boundaries
/// are aligned up.
fn get_chunks_for_regions(
block_size: u32,
file_size: u64,
file_regions: &[Range<u64>],
exact_bounds: bool,
) -> Result<(u32, Vec<ChunkBounds>)> {
let block_size_64 = u64::from(block_size);
let file_blocks: u32 = (file_size / u64::from(block_size))
.try_into()
.map_err(|_| anyhow!("File size {file_size} too large for block size {block_size}"))?;
let mut chunk_list = ChunkList::new();
chunk_list.set_len(file_blocks);
for region in file_regions {
let mut start_byte = region.start;
let mut end_byte = region.end;
if exact_bounds {
if start_byte % block_size_64 != 0 || end_byte % block_size_64 != 0 {
bail!("File region bounds are not block-aligned: {region:?}");
}
} else {
start_byte = start_byte / block_size_64 * block_size_64;
end_byte = padding::round(end_byte, block_size_64).unwrap();
}
let start_block: u32 = (start_byte / block_size_64).try_into().map_err(|_| {
anyhow!("Region start offset {start_byte} too large for block size {block_size}")
})?;
let end_block: u32 = (end_byte / block_size_64).try_into().map_err(|_| {
anyhow!("Region end offset {end_byte} too large for block size {block_size}")
})?;
chunk_list.insert_data(ChunkBounds {
start: start_block,
end: end_block,
});
}
let chunks = chunk_list.iter_allocated().map(|c| c.bounds).collect();
Ok((file_blocks, chunks))
}
/// Compute the sparse [`Chunk`]s needed to cover the specified regions.
fn compute_chunks(
path: &Path,
reader: &mut File,
block_size: u32,
file_blocks: u32,
block_regions: &[ChunkBounds],
cancel_signal: &AtomicBool,
) -> Result<(ChunkList, u32)> {
let mut chunk_list = ChunkList::new();
let mut hasher = Some(Hasher::new());
let mut buf = vec![0u8; block_size as usize];
let mut block = 0;
chunk_list.set_len(file_blocks);
for bounds in block_regions {
if bounds.start != block {
// Not contiguous so we cannot compute the checksum.
hasher = None;
}
let offset = u64::from(bounds.start) * u64::from(block_size);
reader
.seek(SeekFrom::Start(offset))
.with_context(|| format!("Failed to seek file: {path:?}"))?;
for block in *bounds {
stream::check_cancel(cancel_signal)?;
reader
.read_exact(&mut buf)
.with_context(|| format!("Failed to read full block: {path:?}"))?;
if let Some(h) = &mut hasher {
h.update(&buf);
}
let new_bounds = ChunkBounds {
start: block,
end: block + 1,
};
if buf.chunks_exact(4).all(|c| c == &buf[..4]) {
let fill_value = u32::from_le_bytes(buf[..4].try_into().unwrap());
chunk_list.insert_fill(new_bounds, fill_value);
} else {
chunk_list.insert_data(new_bounds);
}
}
block = bounds.end;
}
if block != file_blocks {
hasher = None;
}
let crc32 = hasher.map(|h| h.finalize()).unwrap_or_default();
Ok((chunk_list, crc32))
}
fn unpack_subcommand(
sparse_cli: &SparseCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let reader = open_reader(&cli.input)?;
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate)
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
let mut metadata = Metadata {
header: sparse_reader.header(),
chunks: vec![],
};
let mut writer = open_writer(&cli.output, !cli.preserve)?;
if cli.preserve {
let expected_size =
u64::from(metadata.header.num_blocks) * u64::from(metadata.header.block_size);
let file_size = writer
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.output))?;
if file_size < expected_size {
writer
.set_len(expected_size)
.with_context(|| format!("Failed to set file size: {:?}", cli.output))?;
}
writer
.seek(SeekFrom::Start(0))
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
}
while let Some(chunk) = sparse_reader
.next_chunk()
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
{
match chunk.data {
ChunkData::Fill(value) => {
let fill_value = little_endian::U32::from(value);
let buf = vec![fill_value; metadata.header.block_size as usize / 4];
for _ in chunk.bounds {
stream::check_cancel(cancel_signal)?;
writer
.write_all(buf.as_bytes())
.with_context(|| format!("Failed to write data: {:?}", cli.output))?;
}
}
ChunkData::Data => {
// This cannot overflow.
let to_copy = chunk.bounds.len() * metadata.header.block_size;
stream::copy_n(
&mut sparse_reader,
&mut writer,
to_copy.into(),
cancel_signal,
)
.with_context(|| {
format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output)
})?;
}
ChunkData::Hole => {
// This cannot overflow.
let to_skip = chunk.bounds.len() * metadata.header.block_size;
writer
.seek(SeekFrom::Current(to_skip.into()))
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
}
ChunkData::Crc32(_) => {}
}
metadata.chunks.push(chunk);
}
display_metadata(sparse_cli, &metadata);
sparse_reader
.finish()
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
Ok(())
}
fn pack_subcommand(
sparse_cli: &SparseCli,
cli: &PackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
if cli.block_size == 0 || cli.block_size % 4 != 0 {
bail!(
"Block size must be a non-zero multiple of 4: {}",
cli.block_size,
);
}
let mut reader = open_reader(&cli.input)?;
let file_size = reader
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
if file_size % u64::from(cli.block_size) != 0 {
bail!(
"File size {file_size} is not a multiple of block size {}",
cli.block_size,
);
}
// Compute the byte regions to pack into the sparse file.
let (file_regions, exact_bounds) = if !cli.region.is_empty() {
let regions = cli
.region
.chunks_exact(2)
.map(|c| c[0]..c[1])
.collect::<Vec<_>>();
(regions, false)
} else {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let regions = find_allocated_regions(&cli.input, &reader, cancel_signal)?;
(regions, false)
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
#[allow(clippy::single_range_in_vec_init)]
(vec![0..file_size], true)
}
};
// Get the file regions as non-overlapping and sorted block regions.
let (file_blocks, block_regions) =
get_chunks_for_regions(cli.block_size, file_size, &file_regions, exact_bounds)?;
// Compute the checksum (if possible) and the list of actual chunks.
let (chunk_list, crc32) = compute_chunks(
&cli.input,
&mut reader,
cli.block_size,
file_blocks,
&block_regions,
cancel_signal,
)?;
let chunks = split_chunks(&chunk_list.to_chunks(), cli.block_size);
let metadata = Metadata {
header: Header {
major_version: sparse::MAJOR_VERSION,
minor_version: sparse::MINOR_VERSION,
block_size: cli.block_size,
num_blocks: chunk_list.len(),
// This can't overflow because the number of chunks is always
// smaller than the number of blocks (because we don't add CRC32
// chunks).
num_chunks: chunks.len() as u32,
// This will be zero if the regions don't span the entire file.
crc32,
},
chunks,
};
display_metadata(sparse_cli, &metadata);
let writer = open_writer(&cli.output, true)?;
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
for chunk in metadata.chunks {
sparse_writer
.start_chunk(chunk)
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
if chunk.data == ChunkData::Data {
let offset = u64::from(chunk.bounds.start) * u64::from(cli.block_size);
reader
.seek(SeekFrom::Start(offset))
.with_context(|| format!("Failed to seek file: {:?}", cli.input))?;
let to_copy = u64::from(chunk.bounds.len()) * u64::from(cli.block_size);
stream::copy_n(&mut reader, &mut sparse_writer, to_copy, cancel_signal).with_context(
|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output),
)?;
}
}
sparse_writer
.finish()
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
Ok(())
}
fn repack_subcommand(
sparse_cli: &SparseCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let reader = open_reader(&cli.input)?;
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Validate)
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
let mut metadata = Metadata {
header: sparse_reader.header(),
chunks: vec![],
};
let writer = open_writer(&cli.output, true)?;
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
while let Some(chunk) = sparse_reader
.next_chunk()
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
{
sparse_writer
.start_chunk(chunk)
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
if chunk.data == ChunkData::Data {
// This cannot overflow.
let to_copy = chunk.bounds.len() * metadata.header.block_size;
stream::copy_n(
&mut sparse_reader,
&mut sparse_writer,
to_copy.into(),
cancel_signal,
)
.with_context(|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output))?;
}
metadata.chunks.push(chunk);
}
display_metadata(sparse_cli, &metadata);
sparse_reader
.finish()
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
sparse_writer
.finish()
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
Ok(())
}
fn info_subcommand(sparse_cli: &SparseCli, cli: &InfoCli) -> Result<()> {
let reader = open_reader(&cli.input)?;
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Ignore)
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
let mut metadata = Metadata {
header: sparse_reader.header(),
chunks: vec![],
};
while let Some(chunk) = sparse_reader
.next_chunk()
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
{
metadata.chunks.push(chunk);
}
display_metadata(sparse_cli, &metadata);
Ok(())
}
pub fn sparse_main(cli: &SparseCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
SparseCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
SparseCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
SparseCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
SparseCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack a sparse image.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output raw image.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Preserve existing data in the output file.
///
/// This is useful when unpacking multiple sparse files into a single output
/// file because they contain disjoint blocks of data.
#[arg(long)]
preserve: bool,
}
/// Pack a sparse image.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output sparse image.
///
/// If `--region` is not used and the input file is not a (native) sparse
/// file on Linux, then the output sparse image is written with a CRC32
/// checksum in the header.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input raw image.
///
/// On Linux, if this is a (native) sparse file, then the unallocated
/// sections of the file will be skipped and will be stored in the output
/// file as hole chunks.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Block size.
#[arg(short, long, value_name = "BYTES", default_value_t = 4096)]
block_size: u32,
/// Pack certain byte regions from the file.
///
/// The start offset will be aligned down to the block size and the end
/// offset will be aligned up. This option can be specified any number of
/// times and in any order. Overlapping regions are allowed.
///
/// Unused regions will be stored in the sparse file as hole chunks.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
region: Vec<u64>,
}
/// Repack a sparse image.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
/// Display sparse image metadata.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum SparseCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect sparse images.
#[derive(Debug, Parser)]
pub struct SparseCli {
#[command(subcommand)]
command: SparseCommand,
/// Don't print sparse image metadata.
#[arg(short, long, global = true)]
quiet: bool,
}
+758
View File
@@ -0,0 +1,758 @@
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
env::{self, VarError},
ffi::{OsStr, OsString},
fs::{self, File, OpenOptions},
io::{self, Read, Write},
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
time::Duration,
};
use cms::{
cert::{CertificateChoices, IssuerAndSerialNumber},
content_info::{CmsVersion, ContentInfo},
signed_data::{
CertificateSet, DigestAlgorithmIdentifiers, EncapsulatedContentInfo, SignatureValue,
SignedData, SignerIdentifier, SignerInfo, SignerInfos,
},
};
use passterm::PromptError;
use pkcs8::{
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
LineEnding, PrivateKeyInfo,
pkcs5::{pbes2, scrypt},
};
use rand::RngCore;
use rsa::{
Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, pkcs1v15::SigningKey, traits::PublicKeyParts,
};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
use thiserror::Error;
use x509_cert::{
Certificate,
builder::{Builder, CertificateBuilder, Profile},
der::{Any, Decode, DecodePem, EncodePem, pem::PemLabel, referenced::OwnedToRef},
serial_number::SerialNumber,
spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned},
time::Validity,
};
use crate::util::DebugString;
#[derive(Debug, Error)]
pub enum Error {
#[error("Signature algorithm not supported: {0:?}")]
UnsupportedAlgorithm(SignatureAlgorithm),
#[error("RSA key size ({}) not supported", .0 * 8)]
UnsupportedKeySize(usize),
#[error("Invalid digest length ({0} bytes) for {1:?}")]
InvalidDigestLength(usize, SignatureAlgorithm),
#[error("Invalid signature length ({0} bytes) for {1:?}")]
InvalidSignatureLength(usize, SignatureAlgorithm),
#[error("Failed to run command: {0:?}")]
CommandSpawn(DebugString, #[source] io::Error),
#[error("Command failed with status: {1}: {0:?}")]
CommandExecution(DebugString, ExitStatus),
#[error("Signature from signing helper does not match public key: {0:?}")]
SigningHelperBadSignature(PathBuf),
#[error("Passphrase prompt requires an interactive terminal")]
NotInteractive(#[source] io::Error),
#[error("Failed to prompt for passphrase")]
PassphrasePrompt(#[source] PromptError),
#[error("Passphrases do not match")]
ConfirmPassphrase,
#[error("Failed to read environment variable: {0:?}")]
InvalidEnvVar(OsString, #[source] VarError),
#[error("PEM has start tag, but no end tag")]
PemNoEndTag,
#[error("Failed to load encrypted RSA private key")]
LoadKeyEncrypted(#[source] pkcs8::Error),
#[error("Failed to load unencrypted RSA private key")]
LoadKeyUnencrypted(#[source] pkcs8::Error),
#[error("Failed to save encrypted RSA private key")]
SaveKeyEncrypted(#[source] pkcs8::Error),
#[error("Failed to save unencrypted RSA private key")]
SaveKeyUnencrypted(#[source] pkcs8::Error),
#[error("Failed to load RSA public key")]
LoadPubKey(#[source] pkcs8::spki::Error),
#[error("Failed to save RSA public key")]
SavePubKey(#[source] pkcs8::spki::Error),
#[error("Failed to load X509 certificate")]
LoadCert(#[source] x509_cert::der::Error),
#[error("Failed to save X509 certificate")]
SaveCert(#[source] x509_cert::der::Error),
#[error("Failed to generate RSA key")]
RsaGenerate(#[source] Box<rsa::Error>),
#[error("Failed to RSA sign digest")]
RsaSign(#[source] Box<rsa::Error>),
#[error("Failed to RSA verify signature")]
RsaVerify(#[source] Box<rsa::Error>),
#[error("Failed to generate X509 certificate")]
CertGenerate(#[source] x509_cert::builder::Error),
#[error("Invalid parameters for X509 certificate generation")]
CertParams(#[source] x509_cert::der::Error),
#[error("Failed to CMS sign digest")]
CmsSign(#[source] x509_cert::der::Error),
#[error("Failed to parse CMS signature")]
CmsParse(#[source] x509_cert::der::Error),
#[error("Failed to read file: {0:?}")]
ReadFile(PathBuf, #[source] io::Error),
#[error("Failed to write file: {0:?}")]
WriteFile(PathBuf, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum SignatureAlgorithm {
Sha1WithRsa,
Sha256WithRsa,
Sha512WithRsa,
}
impl SignatureAlgorithm {
/// Length of digest required by the signing algorithm.
pub fn digest_len(self) -> usize {
match self {
Self::Sha1WithRsa => Sha1::output_size(),
Self::Sha256WithRsa => Sha256::output_size(),
Self::Sha512WithRsa => Sha512::output_size(),
}
}
/// Compute the digest of the specified data.
pub fn hash(self, data: &[u8]) -> Vec<u8> {
match self {
Self::Sha1WithRsa => Sha1::digest(data).to_vec(),
Self::Sha256WithRsa => Sha256::digest(data).to_vec(),
Self::Sha512WithRsa => Sha512::digest(data).to_vec(),
}
}
}
#[derive(Clone)]
pub enum PassphraseSource {
Prompt(String),
EnvVar(OsString),
File(PathBuf),
}
impl PassphraseSource {
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
#[allow(clippy::option_if_let_else)]
if let Some(v) = env_var {
Self::EnvVar(v.to_owned())
} else if let Some(p) = pass_file {
Self::File(p.to_owned())
} else {
Self::Prompt(format!("Enter passphrase for {key_file:?}: "))
}
}
fn prompt(prompt: &str) -> Result<String> {
match passterm::prompt_password_tty(Some(prompt)) {
Ok(p) => Ok(p),
Err(e) => {
#[cfg(unix)]
if let PromptError::IOError(io_e) = e {
if let Some(errno) = io_e.raw_os_error() {
if errno == libc::ENXIO || errno == libc::ENOTTY {
return Err(Error::NotInteractive(io_e));
}
}
return Err(Error::PassphrasePrompt(PromptError::IOError(io_e)));
}
Err(Error::PassphrasePrompt(e))
}
}
}
pub fn acquire(&self, confirm: bool) -> Result<String> {
let passphrase = match self {
Self::Prompt(p) => {
let first = Self::prompt(p)?;
if confirm {
let second = Self::prompt("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)
.map_err(|e| Error::ReadFile(p.clone(), e))?
.trim_end_matches(['\r', '\n'])
.to_owned(),
};
Ok(passphrase)
}
}
fn check_key_size(size: usize) -> Result<()> {
// RustCrypto does not support 8192-bit keys.
if size > 4096 / 8 {
return Err(Error::UnsupportedKeySize(size));
}
Ok(())
}
/// Copied from rsa-0.9.6 since the function is not exported.
fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> rsa::Result<Vec<u8>> {
let hash_len = hashed.len();
let t_len = prefix.len() + hashed.len();
if k < t_len + 11 {
return Err(rsa::Error::MessageTooLong);
}
// EM = 0x00 || 0x01 || PS || 0x00 || T
let mut em = vec![0xff; k];
em[0] = 0;
em[1] = 1;
em[k - t_len - 1] = 0;
em[k - t_len..k - hash_len].copy_from_slice(prefix);
em[k - hash_len..k].copy_from_slice(hashed);
Ok(em)
}
#[derive(Clone)]
pub enum RsaSigningKey {
Internal(RsaPrivateKey),
External {
program: PathBuf,
public_key_file: PathBuf,
public_key: RsaPublicKey,
passphrase_source: PassphraseSource,
},
}
impl RsaSigningKey {
/// Size of key in bytes.
pub fn size(&self) -> usize {
match self {
Self::Internal(key) => key.size(),
Self::External { public_key, .. } => public_key.size(),
}
}
/// Get the public key portion of the signing key.
pub fn to_public_key(&self) -> RsaPublicKey {
match self {
Self::Internal(key) => key.to_public_key(),
Self::External { public_key, .. } => public_key.clone(),
}
}
/// Sign the digest with the specified signature algorithm.
pub fn sign(&self, algo: SignatureAlgorithm, digest: &[u8]) -> Result<Vec<u8>> {
if digest.len() != algo.digest_len() {
return Err(Error::InvalidDigestLength(digest.len(), algo));
}
check_key_size(self.size())?;
let scheme = match algo {
// We don't support signing with insecure algorithms.
SignatureAlgorithm::Sha1WithRsa => return Err(Error::UnsupportedAlgorithm(algo)),
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
};
match self {
Self::Internal(key) => key
.sign(scheme, digest)
.map_err(|e| Error::RsaSign(Box::new(e))),
Self::External {
program,
public_key,
public_key_file,
passphrase_source,
} => {
let key_bits = public_key.size() * 8;
let algo_str = match algo {
SignatureAlgorithm::Sha1WithRsa => unreachable!(),
SignatureAlgorithm::Sha256WithRsa => format!("SHA256_RSA{key_bits}"),
SignatureAlgorithm::Sha512WithRsa => format!("SHA512_RSA{key_bits}"),
};
let mut command = Command::new(program);
command.arg(algo_str);
command.arg(public_key_file);
match passphrase_source {
PassphraseSource::Prompt(_) => {}
PassphraseSource::EnvVar(v) => {
command.arg("env");
command.arg(v);
}
PassphraseSource::File(p) => {
command.arg("file");
command.arg(p);
}
}
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::inherit());
let mut child = command
.spawn()
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
// We don't bother with spawning a thread. The pipe capacity on
// all major OSs is significantly larger than the digest, so we
// don't risk deadlocking even if the process doesn't read from
// stdin.
//
// Pipe capacities:
// * Linux: 64 KiB
// * macOS: 4 KiB, 16 KiB (usually), or 64 KiB
// * Windows: 4 KiB
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())
.map_err(|e| Error::RsaSign(Box::new(e)))?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&padded_digest)
.map_err(|e| Error::WriteFile("<signing helper stdin>".into(), e))?;
let child = child
.wait_with_output()
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
if !child.status.success() {
return Err(Error::CommandExecution(
DebugString::new(&command),
child.status,
));
} else if child.stdout.len() != self.size() {
return Err(Error::InvalidSignatureLength(child.stdout.len(), algo));
}
// Check that the helper signed with the proper key.
if let Err(e) = self.to_public_key().verify_sig(algo, digest, &child.stdout) {
return match e {
Error::RsaVerify(_) => {
Err(Error::SigningHelperBadSignature(public_key_file.clone()))
}
e => Err(e),
};
}
Ok(child.stdout)
}
}
}
}
pub trait RsaPublicKeyExt {
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()>;
}
impl RsaPublicKeyExt for RsaPublicKey {
/// Verify the signature against the specified key.
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()> {
// Check this explicitly so we can provide a better error message.
if digest.len() != algo.digest_len() {
return Err(Error::InvalidDigestLength(digest.len(), algo));
}
check_key_size(self.size())?;
let scheme = match algo {
SignatureAlgorithm::Sha1WithRsa => Pkcs1v15Sign::new::<Sha1>(),
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
};
self.verify(scheme, digest, signature)
.map_err(|e| Error::RsaVerify(Box::new(e)))
}
}
/// Generate an 4096-bit RSA key pair.
pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
let mut rng = rand::thread_rng();
// avbroot supports 4096-bit keys only.
let key = RsaPrivateKey::new(&mut rng, 4096).map_err(|e| Error::RsaGenerate(Box::new(e)))?;
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()
.map_err(Error::SavePubKey)?;
let signing_key = SigningKey::<Sha256>::new(key.clone());
let builder = CertificateBuilder::new(
Profile::Root,
SerialNumber::from(serial),
Validity::from_now(validity).map_err(Error::CertParams)?,
subject.parse().map_err(Error::CertParams)?,
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())
.map_err(Error::CertParams)?,
&signing_key,
)
.map_err(Error::CertGenerate)?;
let mut rng = rand::thread_rng();
let cert = builder
.build_with_rng(&mut rng)
.map_err(Error::CertGenerate)?;
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(path: &Path, mut reader: impl Read) -> Result<Certificate> {
let mut data = vec![];
reader
.read_to_end(&mut data)
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
let data = reformat_pem(&data)?;
let certificate = Certificate::from_pem(data).map_err(Error::LoadCert)?;
Ok(certificate)
}
/// Write PEM-encoded certificate to a writer.
pub fn write_pem_cert(path: &Path, mut writer: impl Write, cert: &Certificate) -> Result<()> {
let data = cert.to_pem(LineEnding::LF).map_err(Error::SaveCert)?;
writer
.write_all(data.as_bytes())
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
Ok(())
}
/// Read PEM-encoded certificate from a file.
pub fn read_pem_cert_file(path: &Path) -> Result<Certificate> {
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
read_pem_cert(path, reader)
}
/// Write PEM-encoded certificate to a file.
pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
write_pem_cert(path, writer, cert)
}
/// Read PEM-encoded PKCS8 public key from a reader.
pub fn read_pem_public_key(path: &Path, mut reader: impl Read) -> Result<RsaPublicKey> {
let mut data = String::new();
reader
.read_to_string(&mut data)
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
let key = RsaPublicKey::from_public_key_pem(&data).map_err(Error::LoadPubKey)?;
Ok(key)
}
/// Write PEM-encoded PKCS8 public key to a writer.
pub fn write_pem_public_key(path: &Path, mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
let data = key
.to_public_key_pem(LineEnding::LF)
.map_err(Error::SavePubKey)?;
writer
.write_all(data.as_bytes())
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
Ok(())
}
/// Read PEM-encoded PKCS8 public key from a file.
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
read_pem_public_key(path, reader)
}
/// Write PEM-encoded PKCS8 public key to a file.
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
write_pem_public_key(path, writer, key)
}
/// Read PEM-encoded PKCS8 private key from a reader.
pub fn read_pem_key(
path: &Path,
mut reader: impl Read,
source: &PassphraseSource,
) -> Result<RsaPrivateKey> {
let mut data = String::new();
reader
.read_to_string(&mut data)
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
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(
path: &Path,
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)
.map_err(pkcs8::Error::Asn1)
.map_err(Error::SaveKeyEncrypted)?
};
writer
.write_all(data.as_bytes())
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
Ok(())
}
/// Read PEM-encoded PKCS8 private key from a file.
pub fn read_pem_key_file(path: &Path, source: &PassphraseSource) -> Result<RsaPrivateKey> {
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
read_pem_key(path, 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 writer = options
.open(path)
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
write_pem_key(path, writer, key, source)
}
/// Get the RSA public key from a certificate.
pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
let public_key =
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())
.map_err(Error::LoadPubKey)?;
Ok(public_key)
}
/// Check if a certificate matches a private key.
pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool> {
let public_key = get_public_key(cert)?;
Ok(key.to_public_key() == public_key)
}
/// Parse a CMS [`SignedData`] structure from raw DER-encoded data.
pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
let ci = ContentInfo::from_der(data).map_err(Error::CmsParse)?;
let sd = ci
.content
.decode_as::<SignedData>()
.map_err(Error::CmsParse)?;
Ok(sd)
}
/// Get an iterator to all standard X509 certificates contained within a
/// [`SignedData`] structure.
pub fn iter_cms_certs(sd: &SignedData) -> impl Iterator<Item = &Certificate> {
sd.certificates.iter().flat_map(|certs| {
certs.0.iter().filter_map(|cc| {
if let CertificateChoices::Certificate(c) = cc {
Some(c)
} else {
None
}
})
})
}
/// 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: &RsaSigningKey,
cert: &Certificate,
digest: &[u8],
) -> Result<ContentInfo> {
let signature = key.sign(SignatureAlgorithm::Sha256WithRsa, 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()])
.map_err(Error::CmsSign)?,
encap_content_info: EncapsulatedContentInfo {
econtent_type: const_oid::db::rfc5911::ID_DATA,
econtent: None,
},
certificates: Some(
CertificateSet::try_from(vec![CertificateChoices::Certificate(cert.clone())])
.map_err(Error::CmsSign)?,
),
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).map_err(Error::CmsSign)?,
unsigned_attrs: None,
}])
.map_err(Error::CmsSign)?,
};
let signed_data = ContentInfo {
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
content: Any::encode_from(&signed_data).map_err(Error::CmsSign)?,
};
Ok(signed_data)
}
+161
View File
@@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{fmt, marker::PhantomData};
use bstr::{ByteSlice, ByteVec};
use serde::{Deserializer, Serializer, de::Visitor};
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<T> Visitor<'_> 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
+236
View File
@@ -0,0 +1,236 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Read, Seek, Write};
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use liblzma::{
read::XzDecoder,
stream::{Check, Stream},
write::XzEncoder,
};
use lz4_flex::frame::FrameDecoder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::stream::ReadFixedSizeExt;
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown compression format")]
UnknownFormat,
#[error("I/O error when autodetecting compression format")]
AutoDetect(#[source] io::Error),
#[error("Failed to initialize legacy LZ4 encoder")]
Lz4Init(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
}
type Result<T> = std::result::Result<T, Error>;
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_all(&(compressed.len() as u32).to_le_bytes())?;
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 magic = reader.read_array_exact::<6>().map_err(Error::AutoDetect)?;
reader.rewind().map_err(Error::AutoDetect)?;
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 => {
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
Ok(Self::Lz4Legacy(encoder))
}
CompressedFormat::Xz => {
// Some kernels are compiled without support for the default CRC64.
let stream = Stream::new_easy_encoder(6, Check::Crc32).map_err(Error::XzInit)?;
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(),
}
}
}
+839
View File
@@ -0,0 +1,839 @@
// SPDX-FileCopyrightText: 2023-2024 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 zerocopy::{FromBytes, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
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("Path is not NULL-terminated: {:?}", .0.as_bstr())]
PathNotNullTerminated(Vec<u8>),
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
HardLinksNotSupported(Vec<u8>),
#[error("Entry of type {0} should not have data: {path:?}", path = .1.as_bstr())]
EntryHasData(CpioEntryType, Vec<u8>),
#[error("No inodes available for device {major:x},{minor:x}")]
DeviceFull { major: u32, minor: u32 },
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("{0:?} contains invalid hex integer")]
InvalidHexInt(&'static str, #[source] InvalidHexCharError),
#[error("Failed to read cpio data: {0}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write cpio data: {0}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
#[error("{0:?}: Invalid hex char: {1:?}")]
pub struct InvalidHexCharError(RawHexU32, char);
/// ASCII-encoded hex integer value used in cpio header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHexU32([u8; 8]);
impl fmt::Debug for RawHexU32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0.as_bstr())
}
}
#[allow(clippy::fallible_impl_from)]
impl From<u32> for RawHexU32 {
fn from(mut value: u32) -> Self {
let mut buf = [b'0'; 8];
let mut index = 7;
while value != 0 {
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
value >>= 4;
index -= 1;
}
Self(buf)
}
}
impl TryFrom<RawHexU32> for u32 {
type Error = InvalidHexCharError;
fn try_from(raw_value: RawHexU32) -> std::result::Result<Self, Self::Error> {
let mut value = 0;
for b in raw_value.0 {
let c = b as char;
let digit = c.to_digit(16).ok_or(InvalidHexCharError(raw_value, c))?;
value <<= 4;
value |= digit;
}
Ok(value)
}
}
/// Raw on-disk layout for the cpio header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
magic: [u8; 6],
inode: RawHexU32,
mode: RawHexU32,
uid: RawHexU32,
gid: RawHexU32,
nlink: RawHexU32,
mtime: RawHexU32,
file_size: RawHexU32,
dev_maj: RawHexU32,
dev_min: RawHexU32,
rdev_maj: RawHexU32,
rdev_min: RawHexU32,
path_size: RawHexU32,
crc32: RawHexU32,
}
/// Read a chunk of bytes from the reader. If `size` is less than
/// [`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(Error::IntOverflow("data_size"))?,
};
Ok(size)
}
fn is_size(&self) -> bool {
matches!(self, Self::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 header =
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
if header.magic != *MAGIC_NEW && header.magic != *MAGIC_NEW_CRC {
return Err(Error::UnknownMagic(header.magic));
}
macro_rules! get_field {
($name:ident) => {
let $name = u32::try_from(header.$name)
.map_err(|e| Error::InvalidHexInt(stringify!($name), e))?;
};
}
get_field!(inode);
get_field!(mode);
get_field!(uid);
get_field!(gid);
get_field!(nlink);
get_field!(mtime);
get_field!(file_size);
get_field!(dev_maj);
get_field!(dev_min);
get_field!(rdev_maj);
get_field!(rdev_min);
get_field!(path_size);
get_field!(crc32);
let mut path = read_data(
&mut reader,
path_size.to_usize().unwrap(),
&AtomicBool::new(false),
)
.map_err(|e| Error::DataRead("path", e))?;
if path.last() != Some(&b'\0') {
return Err(Error::PathNotNullTerminated(path));
}
path.pop();
padding::read_discard(&mut reader, 4).map_err(|e| Error::DataRead("path_padding", e))?;
let file_type = CpioEntryType::from_mode(mode);
let data = match file_type {
// 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),
)
.map_err(|e| Error::DataRead("content", e))?;
padding::read_discard(&mut reader, 4)
.map_err(|e| Error::DataRead("content_padding", e))?;
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(Error::IntOverflow("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()));
}
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
let raw_header = RawHeader {
magic: if self.crc32 == 0 {
*MAGIC_NEW
} else {
*MAGIC_NEW_CRC
},
inode: self.inode.into(),
mode: mode.into(),
uid: self.uid.into(),
gid: self.gid.into(),
nlink: self.nlink.into(),
mtime: self.mtime.into(),
file_size: file_size.into(),
dev_maj: self.dev_maj.into(),
dev_min: self.dev_min.into(),
rdev_maj: self.rdev_maj.into(),
rdev_min: self.rdev_min.into(),
path_size: path_size.into(),
crc32: self.crc32.into(),
};
raw_header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("header", e))?;
writer
.write_all(&self.path)
.map_err(|e| Error::DataWrite("path", e))?;
writer
.write_zeros_exact(1)
.map_err(|e| Error::DataWrite("path", e))?;
padding::write_zeros(&mut writer, 4).map_err(|e| Error::DataWrite("path_padding", e))?;
if let CpioEntryData::Data(d) = &self.data {
writer
.write_all(d)
.map_err(|e| Error::DataWrite("content", e))?;
padding::write_zeros(&mut writer, 4)
.map_err(|e| Error::DataWrite("content_padding", e))?;
}
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()
.map_err(|e| Error::DataRead("content", e))?;
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()
.map_err(|e| Error::DataWrite("content", e))?;
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()
.map_err(|e| Error::DataWrite("content", e))?;
self.start_entry(&CpioEntry::new_trailer())?;
// Pad until the end of the block.
if self.pad_to_block_size {
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)
.map_err(|e| Error::DataWrite("block_padding", e))?;
}
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).map_err(|e| Error::DataRead("entry", e))?;
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
return Err(Error::HardLinksNotSupported(entry.path));
}
if let CpioEntryData::Size(s) = entry.data {
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)
.map_err(|e| Error::DataWrite("data", e))?;
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 {
major: entry.dev_maj,
minor: 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).map_err(|e| Error::DataWrite("entry", e))?;
cpio_writer.start_entry(entry)?;
// CpioEntryData::Data will have already been written.
}
cpio_writer.finish()?;
Ok(())
}
+968
View File
@@ -0,0 +1,968 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashSet,
fmt,
io::{self, Read, Seek, SeekFrom, Write},
mem,
ops::Range,
sync::atomic::AtomicBool,
};
use num_traits::ToPrimitive;
use rayon::{
prelude::{IndexedParallelIterator, ParallelIterator},
slice::{ParallelSlice, ParallelSliceMut},
};
use thiserror::Error;
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::verityrs,
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
util::{self, NumBytes, OutOfBoundsError},
};
// Not to be confused with the 255-byte RS block size.
const FEC_BLOCK_SIZE: usize = 4096;
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")]
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Failed to reopen input file")]
InputReopen(#[source] io::Error),
#[error("Failed to reopen output file")]
OutputReopen(#[source] io::Error),
#[error("Failed to read FEC data: {0}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write FEC data: {0}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// 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(&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,
});
}
util::check_bounds(block_size, ..=FEC_MAX_BLOCK_SIZE)
.map_err(|e| Error::IntOutOfBounds("block_size", e))?;
let rs_k = 255 - parity;
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
return Err(Error::UnsupportedParity(parity));
}
let blocks = 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(Error::IntOverflow("fec_data_size"))?;
rounds
.checked_mul(u64::from(rs_k))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.ok_or(Error::IntOverflow("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() {
util::check_bounds(last.end, ..=self.file_size)
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
}
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)
.map_err(|e| Error::DataRead("round", e))?;
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)
.map_err(|e| Error::DataRead("round", e))?;
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)
.map_err(|e| Error::DataRead("round", e))?;
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
let parity = usize::from(self.parity());
let mut num_corrected = 0;
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)
.map_err(|e| Error::DataWrite("round", e))?;
}
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).map_err(Error::InputReopen)?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
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).map_err(Error::InputReopen)?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
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).map_err(Error::InputReopen)?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
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).map_err(Error::InputReopen)?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
let writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
self.repair_one_round(reader, writer, round as u64, buf)
})
.collect::<Result<Vec<u64>>>()?
.into_iter()
.sum();
Ok(num_corrected)
}
}
/// Raw on-disk layout for the FEC image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`FEC_MAGIC`].
magic: little_endian::U32,
/// Image version. This should be equal to [`FEC_VERSION`].
version: little_endian::U32,
/// Size of this [`RawHeader`].
header_size: little_endian::U32,
/// Number of parity bytes per 255-byte Reed-Solomon codeword.
parity: little_endian::U32,
/// Size of the FEC data.
fec_size: little_endian::U32,
/// Size of the actual data.
data_size: little_endian::U64,
/// SHA-256 digest of the FEC data.
digest: [u8; 32],
}
/// A type for reading and writing AOSP's standalone FEC image format.
///
/// The FEC data parser in this implementation is strict. All header fields,
/// 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 = input
.reopen_boxed()
.and_then(|mut f| f.seek(SeekFrom::End(0)))
.map_err(Error::InputReopen)?;
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
let fec_data = fec.generate(input, cancel_signal)?;
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<RawHeader> {
let fec_size: u32 =
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
let header = RawHeader {
magic: FEC_MAGIC.into(),
version: FEC_VERSION.into(),
header_size: (mem::size_of::<RawHeader>() as u32).into(),
parity: u32::from(self.parity).into(),
fec_size: fec_size.into(),
data_size: self.data_size.into(),
digest: digest.as_ref().try_into().unwrap(),
};
Ok(header)
}
}
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)
.map_err(|e| Error::DataRead("fec", e))?;
if fec.len() < FEC_BLOCK_SIZE {
return Err(Error::DataTooSmall);
}
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
let (header, _) =
RawHeader::ref_from_prefix(&fec[header1_offset..]).map_err(|_| Error::DataTooSmall)?;
let header_size = header.header_size.get() as usize;
if header_size > FEC_BLOCK_SIZE / 2 {
// ref_from_prefix() already handles the "too small" case.
return Err(Error::InvalidHeaderSize(header.header_size.get()));
}
let header2_offset = fec.len() - header_size;
// Make sure both headers match, accounting for potential custom fields.
let header1_raw = &fec[header1_offset..][..header_size];
let header2_raw = &fec[header2_offset..][..header_size];
if header1_raw != header2_raw {
return Err(Error::HeadersDifferent);
}
if header.magic != FEC_MAGIC {
return Err(Error::InvalidHeaderMagic(header.magic.get()));
}
if header.version != FEC_VERSION {
return Err(Error::UnsupportedHeaderVersion(header.version.get()));
}
let parity: u8 =
util::try_cast(header.parity.get()).map_err(|e| Error::IntOutOfBounds("parity", e))?;
let fec_size = header.fec_size.get() as usize;
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
if fec_size != actual_fec_size {
return Err(Error::InvalidHeaderFecSize {
value: fec_size,
available: actual_fec_size,
});
}
let data_size = header.data_size.get();
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec[..fec_size]);
if header.digest != actual_digest.as_ref() {
return Err(Error::InvalidFecDigest {
expected: hex::encode(header.digest),
actual: hex::encode(actual_digest),
});
}
// Chop off headers.
fec.resize(fec_size, 0);
Ok(Self {
fec,
data_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)
.map_err(|e| Error::DataWrite("fec_data", e))?;
header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("fec_header_1", e))?;
writer
.write_zeros_exact((FEC_BLOCK_SIZE - 2 * header.as_bytes().len()) as u64)
.map_err(|e| Error::DataWrite("fec_header_padding", e))?;
header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("fec_header_2", e))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{
io::{Cursor, Seek},
sync::{Arc, atomic::AtomicBool},
};
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);
}
}
+785
View File
@@ -0,0 +1,785 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
io::{self, Cursor, Read, SeekFrom, Write},
ops::Range,
str,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use rayon::{
iter::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::{
avb,
padding::{self, ZeroPadding},
},
stream::{self, FromReader, ReadFixedSizeExt, ReadSeekReopen, ToWriter},
util::{self, NumBytes, OutOfBoundsError},
};
#[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.as_bstr())]
UnsupportedHashAlgorithm(Vec<u8>),
#[error("{0:?} field is out of bounds")]
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Failed to reopen input file")]
InputReopen(#[source] io::Error),
#[error("Failed to compute hash tree of input file")]
InputDigest(#[source] io::Error),
#[error("Failed to read hash tree data: {0}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write hash tree data: {0}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
pub struct HashTree {
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(Error::IntOverflow("level_size"))?;
// Depending on the chosen block size, the original file size could
// overflow a usize without the first level's size doing the same.
let level_size_usize: usize =
util::try_cast(level_size).map_err(|e| Error::IntOutOfBounds("level_size", e))?;
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() {
util::check_bounds(last.end, ..=image_size)
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
}
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 > u64::from(self.block_size),
"Images smaller than block size must use a normal hash",
);
// Parallelize in larger chunks to avoid too much seek thrashing.
let 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().map_err(Error::InputReopen)?;
let buf = reader
.read_vec_exact(image_size as usize)
.map_err(Error::InputDigest)?;
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,
)
.map_err(Error::InputDigest)?;
} else if let Some(r) = ranges {
// Read partial blocks from file.
let block_ranges = self.blocks_for_ranges(image_size, r)?;
self.hash_partial_level_parallel(
input,
image_size,
&block_ranges,
level_data,
cancel_signal,
)
.map_err(Error::InputDigest)?;
} else {
// Read entire file.
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)
.map_err(Error::InputDigest)?;
}
// 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_or(0, |r| r.end);
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_or(0, |r| r.end);
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_or(0, |r| r.end);
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(())
}
}
/// Raw on-disk layout for our custom hash tree image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
magic: [u8; 16],
/// Image version. This should be equal to [`HashTreeImage::VERSION`].
version: little_endian::U16,
/// Size of the actual data.
image_size: little_endian::U64,
/// Block size.
block_size: little_endian::U32,
/// Hash algorithm.
algorithm: [u8; 16],
/// Salt size.
salt_size: little_endian::U16,
/// Root digest size.
root_digest_size: little_endian::U16,
/// Hash tree size.
hash_tree_size: little_endian::U32,
}
/// A type for reading and writing a custom hash tree image format.
///
/// File format:
/// - [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("image_size", &self.image_size)
.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;
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::digest_algorithm(name)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
}
/// Generate hash tree data for a file.
pub fn generate(
input: &(dyn ReadSeekReopen + Sync),
block_size: u32,
algorithm: &str,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> Result<Self> {
let image_size = input
.reopen_boxed()
.and_then(|mut f| f.seek(SeekFrom::End(0)))
.map_err(Error::InputReopen)?;
let digest_algorithm = Self::digest_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
Ok(Self {
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 digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
self.root_digest = hash_tree.update(
input,
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 digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, digest_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 header =
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
if header.magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(header.magic));
}
if header.version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(header.version.get()));
}
let algorithm = header.algorithm.trim_end_padding();
let algorithm = str::from_utf8(algorithm)
.map_err(|_| Error::UnsupportedHashAlgorithm(algorithm.to_vec()))?;
let salt = reader
.read_vec_exact(usize::from(header.salt_size))
.map_err(|e| Error::DataRead("header", e))?;
let root_digest = reader
.read_vec_exact(usize::from(header.root_digest_size))
.map_err(|e| Error::DataRead("root_digest", e))?;
let hash_tree = reader
.read_vec_exact(header.hash_tree_size.get() as usize)
.map_err(|e| Error::DataRead("hash_tree", e))?;
Ok(Self {
image_size: header.image_size.get(),
block_size: header.block_size.get(),
algorithm: algorithm.to_owned(),
salt,
root_digest,
hash_tree,
})
}
}
impl<W: Write> ToWriter<W> for HashTreeImage {
type Error = Error;
fn to_writer(&self, mut writer: W) -> Result<()> {
let algorithm = self
.algorithm
.as_bytes()
.to_padded_array::<16>()
.ok_or_else(|| Error::UnsupportedHashAlgorithm(self.algorithm.as_bytes().to_vec()))?;
let salt_size: u16 =
util::try_cast(self.salt.len()).map_err(|e| Error::IntOutOfBounds("salt_size", e))?;
let root_digest_size: u16 = util::try_cast(self.root_digest.len())
.map_err(|e| Error::IntOutOfBounds("root_digest_size", e))?;
let hash_tree_size: u32 = util::try_cast(self.hash_tree.len())
.map_err(|e| Error::IntOutOfBounds("hash_tree_size", e))?;
let header = RawHeader {
magic: *Self::MAGIC,
version: Self::VERSION.into(),
image_size: self.image_size.into(),
block_size: self.block_size.into(),
algorithm,
salt_size: salt_size.into(),
root_digest_size: root_digest_size.into(),
hash_tree_size: hash_tree_size.into(),
};
header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("header", e))?;
writer
.write_all(&self.salt)
.map_err(|e| Error::DataWrite("salt", e))?;
writer
.write_all(&self.root_digest)
.map_err(|e| Error::DataWrite("root_digest", e))?;
writer
.write_all(&self.hash_tree)
.map_err(|e| Error::DataWrite("hash_tree", e))?;
Ok(())
}
}
#[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::IntOutOfBounds(_, _))
);
}
#[test]
fn generate_update_verify() {
let cancel_signal = AtomicBool::new(false);
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
let mut input = SharedCursor::new();
// Try input smaller than one block.
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
assert_eq!(
root_digest,
&[
0x15, 0x0f, 0xe5, 0x51, 0x40, 0x30, 0xb1, 0x43, 0x4a, 0x5d, 0xea, 0xf4, 0x91, 0xec,
0xe9, 0x2c, 0x0e, 0x64, 0x97, 0x44, 0x7d, 0x6d, 0xe7, 0xbd, 0x6b, 0xa8, 0x5e, 0x8c,
0xae, 0x1e, 0x00, 0xa3
],
);
assert_eq!(hash_tree_data, &[]);
// Try larger input that spans multiple blocks are results in an actual
// hash tree being created.
input.write_all(&b"Data".repeat(25)).unwrap();
let (root_digest, mut hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
assert_eq!(
root_digest,
&[
0x92, 0xc3, 0xd7, 0x4a, 0x64, 0x03, 0x4b, 0xcc, 0xa9, 0x9a, 0x44, 0xf6, 0x81, 0xa2,
0x4d, 0xdd, 0x97, 0xd3, 0xda, 0x84, 0xdc, 0xe2, 0x1b, 0x83, 0xd1, 0x7b, 0xab, 0x60,
0x59, 0xe8, 0x45, 0x59
],
);
assert_eq!(
hash_tree_data,
&[
0x7e, 0x33, 0x47, 0xb6, 0xf3, 0x7c, 0xde, 0x0e, 0xe2, 0x8d, 0x9e, 0x49, 0x8e, 0xd4,
0xbd, 0x53, 0x3a, 0xa1, 0xff, 0xeb, 0x4f, 0x6d, 0x5a, 0x5f, 0x55, 0x28, 0x37, 0x79,
0xd0, 0x25, 0x07, 0xd5, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
],
);
// Change some data and update the hash tree.
input.rewind().unwrap();
input.write_all(b"Changed").unwrap();
let root_digest = hash_tree
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
.unwrap();
assert_eq!(
root_digest,
&[
0x8d, 0x03, 0xad, 0x18, 0xf2, 0x53, 0x13, 0x59, 0xf5, 0xbf, 0x68, 0x0e, 0x0c, 0x4a,
0x86, 0xe2, 0x6e, 0xaa, 0x3d, 0x4b, 0x0f, 0x1b, 0x57, 0xad, 0x92, 0xe7, 0xbf, 0x3e,
0xa6, 0xb1, 0x2e, 0xcc
],
);
assert_eq!(
hash_tree_data,
&[
0xfe, 0x46, 0xf7, 0x8c, 0xa1, 0xd9, 0xc8, 0xdd, 0x47, 0x9e, 0x6c, 0x32, 0x7c, 0x38,
0x7f, 0x09, 0xe1, 0x58, 0x92, 0xa3, 0xb6, 0xbd, 0x96, 0xef, 0x10, 0xe8, 0x30, 0xb0,
0x37, 0x8d, 0xef, 0x9a, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
],
);
// Updated hash tree should match newly generated tree.
let (new_root_digest, new_hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
assert_eq!(new_root_digest, root_digest);
assert_eq!(new_hash_tree_data, hash_tree_data);
// Data should validate successfully.
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
.unwrap();
// But not if the data is corrupted.
input.rewind().unwrap();
input.write_all(b"Bad").unwrap();
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
.unwrap_err();
}
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod avb;
pub mod bootimage;
pub mod compression;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod lp;
pub mod ota;
pub mod padding;
pub mod payload;
pub mod sparse;
pub mod verityrs;
pub mod zip;
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023-2024 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)
}
pub trait ZeroPadding {
/// Trim trailing zeros. Intermediate zeros before the last non-zero byte
/// are kept.
fn trim_end_padding(&self) -> &[u8];
/// Return the slice as an array padded with zeros at the end.
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]>;
}
impl ZeroPadding for [u8] {
fn trim_end_padding(&self) -> &[u8] {
let first_ending_zero = self
.iter()
.rposition(|b| *b != 0)
.map(|pos| pos + 1)
.unwrap_or_default();
&self[..first_ending_zero]
}
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]> {
if self.len() > N {
return None;
}
let mut result = [0u8; N];
result[..self.len()].copy_from_slice(self);
Some(result)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
// The gf256 library uses compile-time proc macro code generation. Since
// dm-verity supports RS(255, 231) through RS(255, 253), we'll generate RS
// implementations for every supported configuration.
#![allow(non_snake_case)]
use gf256::rs::rs;
use phf::phf_map;
#[rs(block = 255, data = 231)]
mod rs255w231 {}
#[rs(block = 255, data = 232)]
mod rs255w232 {}
#[rs(block = 255, data = 233)]
mod rs255w233 {}
#[rs(block = 255, data = 234)]
mod rs255w234 {}
#[rs(block = 255, data = 235)]
mod rs255w235 {}
#[rs(block = 255, data = 236)]
mod rs255w236 {}
#[rs(block = 255, data = 237)]
mod rs255w237 {}
#[rs(block = 255, data = 238)]
mod rs255w238 {}
#[rs(block = 255, data = 239)]
mod rs255w239 {}
#[rs(block = 255, data = 240)]
mod rs255w240 {}
#[rs(block = 255, data = 241)]
mod rs255w241 {}
#[rs(block = 255, data = 242)]
mod rs255w242 {}
#[rs(block = 255, data = 243)]
mod rs255w243 {}
#[rs(block = 255, data = 244)]
mod rs255w244 {}
#[rs(block = 255, data = 245)]
mod rs255w245 {}
#[rs(block = 255, data = 246)]
mod rs255w246 {}
#[rs(block = 255, data = 247)]
mod rs255w247 {}
#[rs(block = 255, data = 248)]
mod rs255w248 {}
#[rs(block = 255, data = 249)]
mod rs255w249 {}
#[rs(block = 255, data = 250)]
mod rs255w250 {}
#[rs(block = 255, data = 251)]
mod rs255w251 {}
#[rs(block = 255, data = 252)]
mod rs255w252 {}
#[rs(block = 255, data = 253)]
mod rs255w253 {}
pub static FN_ENCODE: phf::Map<u8, fn(&mut [u8])> = phf_map! {
231u8 => rs255w231::encode,
232u8 => rs255w232::encode,
233u8 => rs255w233::encode,
234u8 => rs255w234::encode,
235u8 => rs255w235::encode,
236u8 => rs255w236::encode,
237u8 => rs255w237::encode,
238u8 => rs255w238::encode,
239u8 => rs255w239::encode,
240u8 => rs255w240::encode,
241u8 => rs255w241::encode,
242u8 => rs255w242::encode,
243u8 => rs255w243::encode,
244u8 => rs255w244::encode,
245u8 => rs255w245::encode,
246u8 => rs255w246::encode,
247u8 => rs255w247::encode,
248u8 => rs255w248::encode,
249u8 => rs255w249::encode,
250u8 => rs255w250::encode,
251u8 => rs255w251::encode,
252u8 => rs255w252::encode,
253u8 => rs255w253::encode,
};
pub static FN_IS_CORRECT: phf::Map<u8, fn(&[u8]) -> bool> = phf_map! {
231u8 => rs255w231::is_correct,
232u8 => rs255w232::is_correct,
233u8 => rs255w233::is_correct,
234u8 => rs255w234::is_correct,
235u8 => rs255w235::is_correct,
236u8 => rs255w236::is_correct,
237u8 => rs255w237::is_correct,
238u8 => rs255w238::is_correct,
239u8 => rs255w239::is_correct,
240u8 => rs255w240::is_correct,
241u8 => rs255w241::is_correct,
242u8 => rs255w242::is_correct,
243u8 => rs255w243::is_correct,
244u8 => rs255w244::is_correct,
245u8 => rs255w245::is_correct,
246u8 => rs255w246::is_correct,
247u8 => rs255w247::is_correct,
248u8 => rs255w248::is_correct,
249u8 => rs255w249::is_correct,
250u8 => rs255w250::is_correct,
251u8 => rs255w251::is_correct,
252u8 => rs255w252::is_correct,
253u8 => rs255w253::is_correct,
};
// Each one of these has its own error type, but the functions can only fail one
// way (too many corrupt bytes), so just throw away the error and return an
// Option instead.
#[allow(clippy::type_complexity)]
pub static FN_CORRECT_ERRORS: phf::Map<u8, fn(&mut [u8]) -> Option<usize>> = phf_map! {
231u8 => |data: &mut [u8]| rs255w231::correct_errors(data).ok(),
232u8 => |data: &mut [u8]| rs255w232::correct_errors(data).ok(),
233u8 => |data: &mut [u8]| rs255w233::correct_errors(data).ok(),
234u8 => |data: &mut [u8]| rs255w234::correct_errors(data).ok(),
235u8 => |data: &mut [u8]| rs255w235::correct_errors(data).ok(),
236u8 => |data: &mut [u8]| rs255w236::correct_errors(data).ok(),
237u8 => |data: &mut [u8]| rs255w237::correct_errors(data).ok(),
238u8 => |data: &mut [u8]| rs255w238::correct_errors(data).ok(),
239u8 => |data: &mut [u8]| rs255w239::correct_errors(data).ok(),
240u8 => |data: &mut [u8]| rs255w240::correct_errors(data).ok(),
241u8 => |data: &mut [u8]| rs255w241::correct_errors(data).ok(),
242u8 => |data: &mut [u8]| rs255w242::correct_errors(data).ok(),
243u8 => |data: &mut [u8]| rs255w243::correct_errors(data).ok(),
244u8 => |data: &mut [u8]| rs255w244::correct_errors(data).ok(),
245u8 => |data: &mut [u8]| rs255w245::correct_errors(data).ok(),
246u8 => |data: &mut [u8]| rs255w246::correct_errors(data).ok(),
247u8 => |data: &mut [u8]| rs255w247::correct_errors(data).ok(),
248u8 => |data: &mut [u8]| rs255w248::correct_errors(data).ok(),
249u8 => |data: &mut [u8]| rs255w249::correct_errors(data).ok(),
250u8 => |data: &mut [u8]| rs255w250::correct_errors(data).ok(),
251u8 => |data: &mut [u8]| rs255w251::correct_errors(data).ok(),
252u8 => |data: &mut [u8]| rs255w252::correct_errors(data).ok(),
253u8 => |data: &mut [u8]| rs255w253::correct_errors(data).ok(),
};
+103
View File
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Seek, SeekFrom, Write};
use zip::{
ZipWriter,
result::ZipResult,
write::{FileOptionExtension, FileOptions, StreamWriter},
};
/// A wrapper around a seekable writer. `W` must implement [`Seek`], but only
/// during the creation of a new instance. The resulting type can be stored in a
/// parent container where the generic type does not implement [`Seek`].
pub struct SeekWriter<W: Write> {
inner: W,
seek_fn: fn(&mut W, SeekFrom) -> io::Result<u64>,
}
impl<W: Write> SeekWriter<W> {
pub fn into_inner(self) -> W {
self.inner
}
}
impl<W: Write + Seek> SeekWriter<W> {
pub fn new(inner: W) -> Self {
Self {
inner,
seek_fn: W::seek,
}
}
}
impl<W: Write> Write for SeekWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<W: Write> Seek for SeekWriter<W> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
(self.seek_fn)(&mut self.inner, pos)
}
}
/// This is an ugly hack to have a single type represent both seekable and
/// streaming [`ZipWriter`]s. `W` only needs to implement [`Seek`] when creating
/// a seekable instance via [`Self::new_seekable`].
pub enum ZipWriterWrapper<W: Write> {
Streaming(ZipWriter<StreamWriter<W>>),
Seekable(ZipWriter<SeekWriter<W>>),
}
impl<W: Write + Seek> ZipWriterWrapper<W> {
pub fn new_seekable(inner: W) -> Self {
Self::Seekable(ZipWriter::new(SeekWriter::new(inner)))
}
}
impl<W: Write> ZipWriterWrapper<W> {
pub fn new_streaming(inner: W) -> Self {
Self::Streaming(ZipWriter::new_stream(inner))
}
pub fn start_file(
&mut self,
name: impl ToString,
options: FileOptions<impl FileOptionExtension>,
) -> ZipResult<u64> {
match self {
Self::Streaming(z) => z.start_file(name, options),
Self::Seekable(z) => z.start_file(name, options),
}
}
pub fn finish(self) -> ZipResult<W> {
match self {
Self::Streaming(z) => Ok(z.finish()?.into_inner()),
Self::Seekable(z) => Ok(z.finish()?.into_inner()),
}
}
}
impl<W: Write> Write for ZipWriterWrapper<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Streaming(z) => z.write(buf),
Self::Seekable(z) => z.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Streaming(z) => z.flush(),
Self::Seekable(z) => z.flush(),
}
}
}
+22
View File
@@ -0,0 +1,22 @@
// 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;
+39
View File
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
process::ExitCode,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use tracing::error;
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
fn main() -> ExitCode {
// Set up a cancel signal so we can properly clean up any temporary files.
let cancel_signal = Arc::new(AtomicBool::new(false));
{
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(()) => ExitCode::SUCCESS,
Err(e) => {
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
error!("{e:?}");
} else {
eprintln!("{e:?}");
}
ExitCode::FAILURE
}
}
}
+51
View File
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
//! Hack to format an integer as an octal string because toml_edit can't output
//! octal-formatted integers and many other toml parsers can't parse it either.
use std::{
fmt::{self, Octal},
marker::PhantomData,
};
use num_traits::{Num, PrimInt};
use serde::{Deserializer, Serializer, de::Visitor};
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<T> Visitor<'_> 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
+6
View File
@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod boot;
pub mod otacert;
pub mod system;
+151
View File
@@ -0,0 +1,151 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
use bitflags::bitflags;
use thiserror::Error;
use tracing::trace;
use x509_cert::{Certificate, der::asn1::BitString};
use zip::{CompressionMethod, DateTime, ZipWriter, result::ZipError, write::SimpleFileOptions};
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("Failed to write otacerts zip")]
ZipWrite(#[source] ZipError),
#[error("Failed to write certificate to otacerts zip")]
CertWrite(#[source] crypto::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// 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 = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(compression_method);
let name = "ota.x509.pem";
writer.start_file(name, options).map_err(Error::ZipWrite)?;
let cert = if flags.is_empty() {
Cow::Borrowed(cert)
} else {
let mut modified = cert.clone();
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
// An empty ASN.1 bit string is always valid.
modified.signature =
BitString::from_bytes(&[]).expect("Empty ASN.1 bit string was invalid");
}
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
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(Path::new(name), &mut writer, &cert).map_err(Error::CertWrite)?;
let raw_writer = writer.finish().map_err(Error::ZipWrite)?;
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))
}
+240
View File
@@ -0,0 +1,240 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
io::{self, Cursor, SeekFrom},
ops::Range,
sync::atomic::AtomicBool,
};
use memchr::memmem;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
use tracing::{Span, debug, debug_span, trace};
use x509_cert::Certificate;
use zip::ZipArchive;
use crate::{
crypto::RsaSigningKey,
format::{
avb::{self, AppendedDescriptorMut, Footer},
ota,
},
patch::otacert,
stream::{self, ReadFixedSizeExt, 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:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Failed to update AVB header")]
AvbUpdate(#[source] avb::Error),
#[error("Failed to generate replacement otacerts zip")]
OtaCertZip(#[source] otacert::Error),
#[error("Failed to read image data")]
ReadData(#[source] io::Error),
#[error("Failed to write image data")]
WriteData(#[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Find the bounds of a non-zip64 zip starting from the EOCD magic offset.
fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
let eocd = &data[eocd_offset..];
if eocd.len() < 22 {
trace!("Buffer is too small to contain EOCD");
return None;
}
let cd_size = u32::from_le_bytes(eocd[12..16].try_into().unwrap()) as usize;
let cd_offset = u32::from_le_bytes(eocd[16..20].try_into().unwrap()) as usize;
let comment_size = usize::from(u16::from_le_bytes(eocd[20..22].try_into().unwrap()));
let start = eocd_offset.checked_sub(cd_size)?.checked_sub(cd_offset)?;
let end = eocd_offset.checked_add(22)?.checked_add(comment_size)?;
if end > data.len() {
trace!("End of zip is out of bounds");
return None;
}
trace!("Found zip bounds: {:?}", start..end);
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
let mut zip_reader = ZipArchive::new(reader).ok()?;
if zip_reader.is_empty() {
// otacerts.zip files contain at least one cert.
trace!("Zip is empty");
return None;
}
for index in 0..zip_reader.len() {
let entry = zip_reader.by_index_raw(index).ok()?;
if !entry.name().ends_with(".x509.pem") {
// otacerts.zip files only contain files named this way.
trace!("Excluded due to invalid name: {:?}", entry.name());
return None;
}
}
debug!("Found otacerts.zip candidate");
// There's one or more entries and every one is named *.x509.pem.
Some(start..end)
}
/// Replace `otacerts.zip` with a new one containing the new certificate, but
/// padded to the same size. If the new zip is too large, the certificate will
/// be modified to remove unnecessary components until it fits. All operations
/// run in parallel where possible. The input and output must refer to the same
/// file and will be reopened from multiple threads.
///
/// Returns two sorted and non-overlapping lists of byte ranges that were
/// modified. The first list are the byte regions within the filesystem data
/// that contained otacerts.zip. The second list is the list of byte regions
/// outside of the filesyste, like the hash tree, FEC data, and AVB metadata.
///
/// If [`Error::OldZipNotFound`] is returned, the output will not have been
/// modified.
#[allow(clippy::type_complexity)]
pub fn patch_system_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
certificate: &Certificate,
key: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
// This ensures that the block containing otacerts.zip's data won't cross
// chunk boundaries.
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
let parent_span = Span::current();
let (mut header, footer, image_size) =
avb::load_image(input.reopen_boxed().map_err(Error::ReadData)?)
.map_err(Error::AvbUpdate)?;
let Some(mut footer) = footer else {
return Err(Error::NoFooter);
};
let AppendedDescriptorMut::HashTree(descriptor) =
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
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).map_err(Error::ReadData)?;
let offset = chunk * CHUNK_SIZE;
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
let mut reader = input.reopen_boxed().map_err(Error::ReadData)?;
reader
.seek(SeekFrom::Start(offset))
.map_err(Error::ReadData)?;
let buf = reader
.read_vec_exact(size as usize)
.map_err(Error::ReadData)?;
let mut writer = output.reopen_boxed().map_err(Error::WriteData)?;
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)
.map_err(Error::OtaCertZip)?;
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
writer
.seek(SeekFrom::Start(bounds.start))
.map_err(Error::WriteData)?;
writer.write_all(&new_zip).map_err(Error::WriteData)?;
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);
}
// Only need to update the hash tree and FEC data corresponding to the
// modified regions.
let update_ranges = Some(modified_ranges.as_slice());
descriptor
.update(input, output, update_ranges, cancel_signal)
.map_err(Error::AvbUpdate)?;
if !header.public_key.is_empty() {
debug!("Signing system image");
header.set_algo_for_key(key).map_err(Error::AvbUpdate)?;
header.sign(key).map_err(Error::AvbUpdate)?;
}
let writer = output.reopen_boxed().map_err(Error::WriteData)?;
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))
.map_err(Error::AvbUpdate)?;
let AppendedDescriptorMut::HashTree(descriptor) =
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
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(Error::IntOverflow("hash_tree_end"))?;
let fec_data_end = descriptor
.fec_offset
.checked_add(descriptor.fec_size)
.ok_or(Error::IntOverflow("fec_data_end"))?;
let header_end = footer
.vbmeta_offset
.checked_add(footer.vbmeta_size)
.ok_or(Error::IntOverflow("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))
}
+15
View File
@@ -0,0 +1,15 @@
#![allow(clippy::all)]
#![allow(clippy::nursery)]
#![allow(clippy::pedantic)]
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"));
}
+869
View File
@@ -0,0 +1,869 @@
// 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::{
Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering},
},
};
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 fixed-size buffers.
pub trait ReadFixedSizeExt {
/// Read fixed-size array.
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]>;
/// Read fixed-sized [`Vec`].
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>>;
}
impl<R: Read> ReadFixedSizeExt for R {
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]> {
let mut buf = [0u8; N];
self.read_exact(&mut buf)?;
Ok(buf)
}
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; size];
self.read_exact(&mut buf)?;
Ok(buf)
}
}
/// 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(Self::new(self.get_ref().reopen()?))
}
}
impl<W: Write + Reopen> Reopen for BufWriter<W> {
fn reopen(&self) -> io::Result<Self> {
Ok(Self::new(self.get_ref().reopen()?))
}
}
/// A reader wrapper that implements [`Seek`], but only for reporting the
/// current file position.
pub struct CountingReader<R> {
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> {
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> {
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> {
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> {
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 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()
}
}
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, PSeekFile, ReadDiscardExt,
Reopen, SectionReader, SharedCursor, 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 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 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);
}
}
+477
View File
@@ -0,0 +1,477 @@
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
fmt::{self, Display},
mem,
ops::{
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
},
path::Path,
};
use num_traits::{NumCast, PrimInt};
use thiserror::Error;
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)
}
}
}
/// Stores a precomputed [`Debug`] string.
#[derive(Clone)]
pub struct DebugString(String);
impl DebugString {
pub fn new(value: impl fmt::Debug) -> Self {
Self(format!("{value:?}"))
}
}
impl fmt::Debug for DebugString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Hash, PartialEq, Eq)]
pub enum AnyRange<T> {
Range(Range<T>),
RangeFrom(RangeFrom<T>),
RangeFull(RangeFull),
RangeInclusive(RangeInclusive<T>),
RangeTo(RangeTo<T>),
RangeToInclusive(RangeToInclusive<T>),
}
impl<T> AnyRange<T> {
pub fn with_bounds(start: Bound<T>, end: Bound<T>) -> Option<Self> {
let result = match (start, end) {
(Bound::Included(s), Bound::Excluded(e)) => Self::Range(s..e),
(Bound::Included(s), Bound::Unbounded) => Self::RangeFrom(s..),
(Bound::Unbounded, Bound::Unbounded) => Self::RangeFull(..),
(Bound::Included(s), Bound::Included(e)) => Self::RangeInclusive(s..=e),
(Bound::Unbounded, Bound::Excluded(e)) => Self::RangeTo(..e),
(Bound::Unbounded, Bound::Included(e)) => Self::RangeToInclusive(..=e),
(Bound::Excluded(_), _) => return None,
};
Some(result)
}
}
impl<T: PartialOrd<T>> AnyRange<T> {
pub fn contains<U>(&self, item: &U) -> bool
where
T: PartialOrd<U>,
U: ?Sized + PartialOrd<T>,
{
<Self as RangeBounds<T>>::contains(self, item)
}
}
impl<T: fmt::Debug> fmt::Debug for AnyRange<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Range(r) => r.fmt(f),
Self::RangeFrom(r) => r.fmt(f),
Self::RangeFull(r) => r.fmt(f),
Self::RangeInclusive(r) => r.fmt(f),
Self::RangeTo(r) => r.fmt(f),
Self::RangeToInclusive(r) => r.fmt(f),
}
}
}
impl<T> RangeBounds<T> for AnyRange<T> {
fn start_bound(&self) -> Bound<&T> {
match self {
Self::Range(r) => r.start_bound(),
Self::RangeFrom(r) => r.start_bound(),
Self::RangeFull(r) => r.start_bound(),
Self::RangeInclusive(r) => r.start_bound(),
Self::RangeTo(r) => r.start_bound(),
Self::RangeToInclusive(r) => r.start_bound(),
}
}
fn end_bound(&self) -> Bound<&T> {
match self {
Self::Range(r) => r.end_bound(),
Self::RangeFrom(r) => r.end_bound(),
Self::RangeFull(r) => r.end_bound(),
Self::RangeInclusive(r) => r.end_bound(),
Self::RangeTo(r) => r.end_bound(),
Self::RangeToInclusive(r) => r.end_bound(),
}
}
}
impl<T> From<Range<T>> for AnyRange<T> {
fn from(value: Range<T>) -> Self {
Self::Range(value)
}
}
impl<T> From<RangeFrom<T>> for AnyRange<T> {
fn from(value: RangeFrom<T>) -> Self {
Self::RangeFrom(value)
}
}
impl<T> From<RangeFull> for AnyRange<T> {
fn from(value: RangeFull) -> Self {
Self::RangeFull(value)
}
}
impl<T> From<RangeInclusive<T>> for AnyRange<T> {
fn from(value: RangeInclusive<T>) -> Self {
Self::RangeInclusive(value)
}
}
impl<T> From<RangeTo<T>> for AnyRange<T> {
fn from(value: RangeTo<T>) -> Self {
Self::RangeTo(value)
}
}
impl<T> From<RangeToInclusive<T>> for AnyRange<T> {
fn from(value: RangeToInclusive<T>) -> Self {
Self::RangeToInclusive(value)
}
}
/// A non-generic type that can represent any 64-bit or smaller primitive
/// integer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LargeInt {
Signed(i64),
Unsigned(u64),
}
impl fmt::Display for LargeInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Signed(n) => n.fmt(f),
Self::Unsigned(n) => n.fmt(f),
}
}
}
/// A non-generic type that can represent any 64-bit or smaller primitive
/// integer range.
#[derive(Clone, PartialEq, Eq)]
pub enum LargeIntRange {
Signed(AnyRange<i64>),
Unsigned(AnyRange<u64>),
}
impl fmt::Debug for LargeIntRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Signed(r) => r.fmt(f),
Self::Unsigned(r) => r.fmt(f),
}
}
}
/// An error returned when a value is not within a specific range.
#[derive(Clone, Debug, Error)]
#[error("Integer value {value} not in bounds: {range:?}")]
pub struct OutOfBoundsError {
value: LargeInt,
range: LargeIntRange,
}
/// Verify that `value` is within `bounds` and then return `value` if it is.
pub fn check_bounds<T: PrimInt>(
value: T,
range: impl Into<AnyRange<T>>,
) -> Result<T, OutOfBoundsError> {
const {
assert!(
mem::size_of::<T>() <= 8,
"Integer must be 64 bits or smaller"
);
}
let range = range.into();
if !range.contains(&value) {
let value = if T::min_value() != T::zero() {
LargeInt::Signed(NumCast::from(value).unwrap())
} else {
LargeInt::Unsigned(NumCast::from(value).unwrap())
};
let range = if T::min_value() != T::zero() {
let start = match range.start_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
let end = match range.end_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
LargeIntRange::Signed(AnyRange::with_bounds(start, end).unwrap())
} else {
let start = match range.start_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
let end = match range.end_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
LargeIntRange::Unsigned(AnyRange::with_bounds(start, end).unwrap())
};
return Err(OutOfBoundsError { value, range });
}
Ok(value)
}
/// Try to cast `value` to primitive integer type `T`. If it does not fit, the
/// error will indicate the valid range of values.
pub fn try_cast<T: PrimInt, V: PrimInt>(value: V) -> Result<T, OutOfBoundsError> {
const {
assert!(
mem::size_of::<T>() <= 8,
"Integer must be 64 bits or smaller"
);
}
NumCast::from(value).ok_or_else(|| {
let value = if V::min_value() != V::zero() {
LargeInt::Signed(NumCast::from(value).unwrap())
} else {
LargeInt::Unsigned(NumCast::from(value).unwrap())
};
let range = if T::min_value() != T::zero() {
let min = NumCast::from(T::min_value()).unwrap();
let max = NumCast::from(T::max_value()).unwrap();
LargeIntRange::Signed((min..=max).into())
} else {
let min = NumCast::from(T::min_value()).unwrap();
let max = NumCast::from(T::max_value()).unwrap();
LargeIntRange::Unsigned((min..=max).into())
};
OutOfBoundsError { value, range }
})
}
/// Check if a byte slice is all zeros.
pub fn is_zero(mut buf: &[u8]) -> bool {
while !buf.is_empty() {
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()
}
pub fn join(into_iter: impl IntoIterator<Item = impl Display>, sep: &str) -> String {
use std::fmt::Write;
let mut result = String::new();
for (i, item) in into_iter.into_iter().enumerate() {
if i > 0 {
result.push_str(sep);
}
write!(result, "{item}").expect("Failed to allocate");
}
result
}
pub fn sort<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
let mut items = iter.collect::<Vec<_>>();
items.sort();
items
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_any_range() {
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Excluded(1)).unwrap();
assert_eq!(range, AnyRange::from(0..1));
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Unbounded).unwrap();
assert_eq!(range, AnyRange::from(0..));
let range = AnyRange::<i32>::with_bounds(Bound::Unbounded, Bound::Unbounded).unwrap();
assert_eq!(range, AnyRange::from(..));
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Included(1)).unwrap();
assert_eq!(range, AnyRange::from(0..=1));
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Excluded(1)).unwrap();
assert_eq!(range, AnyRange::from(..1));
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Included(1)).unwrap();
assert_eq!(range, AnyRange::from(..=1));
}
#[test]
fn test_check_bounds() {
check_bounds(i64::MIN, ..).unwrap();
check_bounds(i64::MAX, ..).unwrap();
check_bounds(u64::MIN, ..).unwrap();
check_bounds(u64::MAX, ..).unwrap();
check_bounds(0, -1..=1).unwrap();
let err = check_bounds(i8::MAX, 0..=0).unwrap_err();
assert_eq!(err.value, LargeInt::Signed(127));
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(0..=0)));
let err = check_bounds(u8::MAX, 0..=0).unwrap_err();
assert_eq!(err.value, LargeInt::Unsigned(255));
assert_eq!(err.range, LargeIntRange::Unsigned(AnyRange::from(0..=0)));
}
#[test]
fn test_try_cast() {
let value: u8 = try_cast(255u16).unwrap();
assert_eq!(value, 255);
let err = try_cast::<i8, _>(256u16).unwrap_err();
assert_eq!(err.value, LargeInt::Unsigned(256));
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(-128..=127)));
}
#[test]
fn test_ranges_overlaps() {
assert!(!ranges_overlaps(&[0..4], &(0..0)));
assert!(ranges_overlaps(&[0..4], &(0..4)));
assert!(ranges_overlaps(&[0..4], &(1..4)));
assert!(ranges_overlaps(&[0..4], &(0..3)));
assert!(!ranges_overlaps(&[0..4], &(4..5)));
assert!(ranges_overlaps(&[5..8], &(5..9)));
assert!(ranges_overlaps(&[5..8], &(4..8)));
assert!(ranges_overlaps(&[5..8], &(4..9)));
assert!(ranges_overlaps(&[0..4, 5..8], &(4..5)));
assert!(ranges_overlaps(&[0..4, 5..8], &(0..9)));
}
#[test]
fn test_ranges_contains() {
assert!(ranges_contains(&[0..4], &0));
assert!(!ranges_contains(&[0..4], &4));
assert!(!ranges_contains(&[0..4, 5..8], &4));
assert!(ranges_contains(&[0..4, 5..8], &6));
}
}
+469
View File
@@ -0,0 +1,469 @@
// 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,
crypto::RsaSigningKey,
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
KernelCmdlineDescriptor, PropertyDescriptor,
},
stream::SharedCursor,
};
fn get_test_key() -> RsaSigningKey {
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",
));
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
RsaSigningKey::Internal(key)
}
fn repeat_str(s: &str, max_len: usize) -> String {
assert!(!s.is_empty());
let mut result = s.repeat(max_len / s.len());
result.push_str(&s[..max_len % s.len()]);
result
}
fn repeat_array<const N: usize>(data: &[u8]) -> [u8; N] {
assert!(!data.is_empty());
let mut result = [0u8; N];
for i in 0..N / data.len() {
result[i * data.len()..][..data.len()].copy_from_slice(data);
}
let remain = N % data.len();
result[N - remain..].copy_from_slice(&data[..remain]);
result
}
#[test]
fn round_trip_root_image() {
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::HashTree(HashTreeDescriptor {
dm_verity_version: 1,
image_size: 4096,
tree_offset: 0,
tree_size: 2048,
data_block_size: 4096,
hash_block_size: 4096,
fec_num_roots: 1,
fec_offset: 2048,
fec_size: 2048,
hash_algorithm: "sha512".to_owned(),
partition_name: "hashtreed_partition".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(8),
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(8),
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
Descriptor::Hash(HashDescriptor {
image_size: 6,
hash_algorithm: "sha256".to_owned(),
partition_name: "hashed_partition".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(4),
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
Descriptor::KernelCmdline(KernelCmdlineDescriptor {
flags: 1,
cmdline: "foobar".to_owned(),
}),
Descriptor::ChainPartition(ChainPartitionDescriptor {
rollback_index_location: 1,
partition_name: "chained_partition".to_owned(),
public_key: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(129),
flags: 0xfedcba98,
reserved: repeat_array(&[0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// 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(),
[
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
0xae, 0x0a, 0x0f, 0x0a, 0x4a, 0x15, 0x91, 0x0e, 0x4d, 0x18, 0x31, 0x48, 0x20, 0xe8,
0x44, 0x62, 0x07, 0x98, 0x43, 0x30, 0xee, 0x2d, 0x20, 0x28, 0xc3, 0x94, 0xc6, 0x0e,
0x86, 0xa3, 0xa7, 0x17, 0x36, 0xfd, 0x50, 0x7c,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
assert_matches!(new_footer, None);
assert_eq!(new_header, header);
assert_eq!(new_image_size, data.len() as u64);
}
#[test]
fn round_trip_appended_hash_image() {
let image_size = 12288;
let raw_data = b"foobar";
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::Hash(HashDescriptor {
image_size: raw_data.len() as u64,
hash_algorithm: "sha256".to_owned(),
partition_name: "vbmeta_appended_hash".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: vec![], // autogenerated
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
version_major: 1,
version_minor: 0,
original_image_size: 0, // autogenerated
vbmeta_offset: 0, // autogenerated
vbmeta_size: 0, // autogenerated
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = Cursor::new(Vec::new());
let cancel_signal = AtomicBool::new(false);
// 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, Some(image_size)).unwrap();
let data = writer.into_inner();
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
0xe1, 0xc5, 0xac, 0x4f, 0x98, 0xb3, 0x0a, 0xed, 0xfc, 0xee, 0xa2, 0x6a, 0x77, 0xf4,
0xe5, 0x69, 0xa0, 0xcd, 0x7a, 0xd1, 0xfe, 0x1d, 0x07, 0xd1, 0x25, 0xc6, 0x22, 0xe0,
0x25, 0xcb, 0xe9, 0x75, 0x50, 0xe4, 0xae, 0x59,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
let new_footer = new_footer.unwrap();
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, image_size);
}
#[test]
fn round_trip_appended_hash_tree_image_fixed_size() {
let image_size = 32768;
let raw_data: [u8; 8192] = repeat_array(b"foobar");
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::HashTree(HashTreeDescriptor {
dm_verity_version: 1,
image_size: raw_data.len() as u64,
tree_offset: 0, // autogenerated
tree_size: 0, // autogenerated
data_block_size: 4096,
hash_block_size: 4096,
fec_num_roots: 2,
fec_offset: 0, // autogenerated
fec_size: 0, // autogenerated
hash_algorithm: "sha256".to_owned(),
partition_name: "vbmeta_appended_hash_tree".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: vec![], // autogenerated
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
version_major: 1,
version_minor: 0,
original_image_size: 0, // autogenerated
vbmeta_offset: 0, // autogenerated
vbmeta_size: 0, // autogenerated
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = SharedCursor::default();
let cancel_signal = AtomicBool::new(false);
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.update(&writer, &writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Verify the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.verify(&writer, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut data).unwrap();
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
0xaf, 0x4c, 0x56, 0x64, 0x3a, 0xd1, 0x06, 0x01, 0xda, 0x2e, 0x05, 0x67, 0xd1, 0x01,
0xe3, 0xcb, 0x7b, 0x1e, 0xeb, 0x05, 0x89, 0xeb, 0x80, 0xcc, 0x17, 0x0c, 0x24, 0x73,
0x0d, 0xcb, 0x36, 0xfa, 0x17, 0xbd, 0x20, 0x7e,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
let new_footer = new_footer.unwrap();
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, image_size);
}
#[test]
fn round_trip_appended_hash_tree_image_minimum_size() {
let raw_data: [u8; 8192] = repeat_array(b"foobar");
let mut header = Header {
required_libavb_version_major: 1,
required_libavb_version_minor: 0,
algorithm_type: AlgorithmType::Sha256Rsa4096,
hash: vec![], // autogenerated
signature: vec![], // autogenerated
public_key: vec![], // autogenerated
public_key_metadata: vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
],
descriptors: vec![
Descriptor::Property(PropertyDescriptor {
key: "foobar".to_owned(),
value: b"Invalid UTF-8: \xFF".to_vec(),
}),
Descriptor::HashTree(HashTreeDescriptor {
dm_verity_version: 1,
image_size: raw_data.len() as u64,
tree_offset: 0, // autogenerated
tree_size: 0, // autogenerated
data_block_size: 4096,
hash_block_size: 4096,
fec_num_roots: 2,
fec_offset: 0, // autogenerated
fec_size: 0, // autogenerated
hash_algorithm: "sha256".to_owned(),
partition_name: "vbmeta_appended_hash_tree".to_owned(),
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
root_digest: vec![], // autogenerated
flags: 0,
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
}),
],
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
version_major: 1,
version_minor: 0,
original_image_size: 0, // autogenerated
vbmeta_offset: 0, // autogenerated
vbmeta_size: 0, // autogenerated
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = SharedCursor::default();
let cancel_signal = AtomicBool::new(false);
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.update(&writer, &writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Verify the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.verify(&writer, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Sign the header.
let key = get_test_key();
header.sign(&key).unwrap();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut data).unwrap();
// Verify checksum of the output.
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
0xee, 0xbf, 0x95, 0x31, 0xff, 0xc7, 0xf5, 0xb9, 0x4f, 0x18, 0x57, 0x46, 0x37, 0xbb,
0xce, 0x7b, 0xa7, 0x26, 0x18, 0x5a, 0x3c, 0x41, 0xb2, 0x2e, 0xb7, 0x86, 0x51, 0xdc,
0xf6, 0x26, 0x86, 0xf3, 0xc7, 0x96, 0x23, 0xed,
],
);
// Parse the generated image.
let mut reader = Cursor::new(&data);
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
let new_footer = new_footer.unwrap();
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, 28672);
}
+349
View File
@@ -0,0 +1,349 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::Cursor;
use avbroot::{
self,
crypto::RsaSigningKey,
format::{
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
bootimage::{
self, BootImage, BootImageExt, BootImageV0Through2, BootImageV3Through4, RamdiskMeta,
V1Extra, V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
},
},
stream::{FromReader, ToWriter},
};
use pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
fn get_test_key() -> RsaSigningKey {
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",
));
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
RsaSigningKey::Internal(key)
}
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);
}
+37
View File
@@ -0,0 +1,37 @@
// 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);
}
+90
View File
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Cursor};
use avbroot::{
self,
format::cpio::{CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
util,
};
fn generate_archive() -> Vec<u8> {
let writer = Cursor::new(Vec::new());
let mut cpio_writer = CpioWriter::new(writer, false);
for entry in [
CpioEntry::new_symlink(b"symlink", b"target"),
CpioEntry::new_directory(b"directory", 0o755),
CpioEntry::new_file(b"file", 0o644, CpioEntryData::Data(b"foobar".to_vec())),
CpioEntry {
path: b"reserved".to_vec(),
data: CpioEntryData::Size(0),
inode: 12345,
file_type: CpioEntryType::Reserved,
file_mode: 0o4777,
uid: 12345678,
gid: 87654321,
nlink: 2,
mtime: 1700000000,
dev_maj: 2222,
dev_min: 3333,
rdev_maj: 4444,
rdev_min: 5555,
crc32: 0xfedcba09,
},
] {
cpio_writer.start_entry(&entry).unwrap();
}
let writer = cpio_writer.finish().unwrap();
let data = writer.into_inner();
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xb0, 0x51, 0xac, 0x28, 0x6f, 0x78, 0xe2, 0xe7, 0x45, 0xa0, 0x52, 0x7c, 0xff, 0x42,
0x30, 0x55, 0xbd, 0x64, 0x7d, 0x4e, 0xb8, 0xe6, 0x95, 0xe5, 0x9b, 0xd1, 0x13, 0xd6,
0x43, 0x0e, 0x32, 0xb2, 0x4e, 0x62, 0xa4, 0x55, 0x64, 0x48, 0xb7, 0x32, 0x26, 0x57,
0x75, 0x07, 0xf5, 0xa6, 0x0f, 0x18, 0xc3, 0x9e, 0x9f, 0x06, 0xdb, 0xa4, 0xf7, 0xeb,
0x5e, 0x8f, 0xce, 0xd0, 0x2b, 0x54, 0x39, 0x57
],
);
data
}
#[test]
fn round_trip_archive() {
let data = generate_archive();
assert_ne!(data.len() % 512, 0);
for pad_to_block_size in [false, true] {
println!("Pad to block size: {pad_to_block_size}");
let reader = Cursor::new(&data);
let mut cpio_reader = CpioReader::new(reader, false);
let writer = Cursor::new(Vec::new());
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
while let Some(entry) = cpio_reader.next_entry().unwrap() {
cpio_writer.start_entry(&entry).unwrap();
if entry.file_type == CpioEntryType::Regular {
io::copy(&mut cpio_reader, &mut cpio_writer).unwrap();
}
}
let writer = cpio_writer.finish().unwrap();
let new_data = writer.get_ref().as_slice();
if pad_to_block_size {
assert!(new_data.starts_with(&data));
assert!(util::is_zero(&new_data[data.len()..]));
assert_eq!(new_data.len() % 512, 0);
} else {
assert_eq!(new_data, data);
}
}
}
+426
View File
@@ -0,0 +1,426 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{io::Cursor, num::NonZeroU64};
use avbroot::{
format::lp::{
BlockDevice, BlockDeviceFlags, Extent, ExtentType, HeaderFlags, ImageType, Metadata,
MetadataSlot, Partition, PartitionAttributes, PartitionGroup, PartitionGroupFlags,
},
stream::{FromReader, ToWriter},
};
fn round_trip(metadata: &Metadata, sha512: &[u8; 64]) {
let mut writer = Cursor::new(Vec::new());
metadata.to_writer(&mut writer).unwrap();
let data = writer.into_inner();
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
sha512,
);
let mut reader = Cursor::new(&data);
let new_metadata = Metadata::from_reader(&mut reader).unwrap();
assert_eq!(&new_metadata, metadata);
}
#[test]
fn round_trip_empty_image() {
// Layout from Google Pixel 9 Pro XL stock factory image:
// komodo-ad1a.240530.047-factory-bb04e484.zip -> super_empty.img
let metadata = Metadata {
image_type: ImageType::Empty,
metadata_max_size: 65536,
metadata_slot_count: 3,
logical_block_size: 4096,
slots: vec![MetadataSlot {
major_version: 10,
minor_version: 2,
groups: vec![
PartitionGroup {
name: "default".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: None,
partitions: vec![],
},
PartitionGroup {
name: "google_dynamic_partitions_a".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_ext_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "product_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
],
},
PartitionGroup {
name: "google_dynamic_partitions_b".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_ext_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "product_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
],
},
],
block_devices: vec![BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 8531214336,
partition_name: "super".into(),
flags: BlockDeviceFlags::empty(),
}],
flags: HeaderFlags::VIRTUAL_AB_DEVICE,
}],
};
// This is semantically equivalent, but not identical. The Metadata data
// structure only retains the order of partitions within a group, but not
// globally. This checksum is meant to protect against unintended future
// changes.
let sha512 = [
0xfa, 0xdf, 0xf2, 0xb6, 0x74, 0xec, 0x78, 0x7d, 0x0f, 0x7d, 0x17, 0x54, 0xcf, 0x1b, 0x53,
0x13, 0x66, 0x13, 0x5e, 0x8e, 0xcc, 0x84, 0xa2, 0x63, 0xaf, 0x0d, 0x68, 0x96, 0xc6, 0x40,
0x4e, 0x83, 0xe4, 0xe9, 0xef, 0x61, 0xdc, 0x2a, 0x25, 0x5f, 0xa2, 0x7d, 0x29, 0x0b, 0xb6,
0x26, 0x93, 0x59, 0xc9, 0xa8, 0x56, 0x3b, 0x3d, 0x3d, 0x15, 0x6b, 0xee, 0x78, 0x56, 0x78,
0xa1, 0x83, 0x6d, 0x70,
];
round_trip(&metadata, &sha512);
}
#[test]
fn round_trip_normal_image() {
// Layout from Google Pixel 9 Pro XL GrapheneOS factory image:
// komodo-install-2024082500.zip -> super_1.img
let slot = MetadataSlot {
major_version: 10,
minor_version: 2,
groups: vec![
PartitionGroup {
name: "default".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: None,
partitions: vec![],
},
PartitionGroup {
name: "google_dynamic_partitions_a".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 2465952,
extent_type: ExtentType::Linear {
start_sector: 2048,
block_device_index: 0,
},
}],
},
Partition {
name: "system_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 23720,
extent_type: ExtentType::Linear {
start_sector: 2469888,
block_device_index: 0,
},
}],
},
Partition {
name: "system_ext_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 786144,
extent_type: ExtentType::Linear {
start_sector: 2494464,
block_device_index: 0,
},
}],
},
Partition {
name: "product_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 1396432,
extent_type: ExtentType::Linear {
start_sector: 3280896,
block_device_index: 0,
},
}],
},
Partition {
name: "vendor_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 1959024,
extent_type: ExtentType::Linear {
start_sector: 4677632,
block_device_index: 0,
},
}],
},
Partition {
name: "vendor_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 55008,
extent_type: ExtentType::Linear {
start_sector: 6637568,
block_device_index: 0,
},
}],
},
],
},
PartitionGroup {
name: "google_dynamic_partitions_b".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_ext_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "product_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
],
},
],
block_devices: vec![BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 8531214336,
partition_name: "super".into(),
flags: BlockDeviceFlags::empty(),
}],
flags: HeaderFlags::VIRTUAL_AB_DEVICE,
};
let metadata = Metadata {
image_type: ImageType::Normal,
metadata_max_size: 65536,
metadata_slot_count: 3,
logical_block_size: 4096,
slots: vec![slot; 3],
};
// This is semantically equivalent, but not identical. The Metadata data
// structure only retains the order of partitions within a group, but not
// globally. This checksum is meant to protect against unintended future
// changes.
let sha512 = [
0x3b, 0xad, 0xd4, 0x22, 0xa1, 0x5a, 0xc5, 0xdf, 0x72, 0x7d, 0x92, 0x35, 0x04, 0x8a, 0x75,
0xd9, 0x33, 0x0d, 0xaa, 0x9e, 0x97, 0xd4, 0x13, 0x28, 0x5e, 0x0f, 0x12, 0x0c, 0xf2, 0xb3,
0xdc, 0x35, 0x89, 0x65, 0x40, 0xb0, 0x67, 0xb1, 0x54, 0x09, 0x52, 0x3e, 0x78, 0x3d, 0x3f,
0xa7, 0xf7, 0xa0, 0x77, 0xa8, 0xfc, 0xb7, 0x93, 0x19, 0xcd, 0x43, 0xea, 0x9a, 0x74, 0x65,
0x54, 0x3c, 0xaa, 0x12,
];
round_trip(&metadata, &sha512);
}
#[test]
fn round_trip_retrofit_image() {
// Layout from Google Pixel 3a XL stock factory image:
// bonito-ota-sp2a.220505.008-37a410d5.zip -> system.img
let slot = MetadataSlot {
major_version: 10,
minor_version: 0,
groups: vec![
PartitionGroup {
name: "default".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: None,
partitions: vec![],
},
PartitionGroup {
name: "google_dynamic_partitions".into(),
flags: PartitionGroupFlags::SLOT_SUFFIXED,
maximum_size: NonZeroU64::new(4068474880),
partitions: vec![
Partition {
name: "system".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![Extent {
num_sectors: 1757416,
extent_type: ExtentType::Linear {
start_sector: 2048,
block_device_index: 0,
},
}],
},
Partition {
name: "vendor".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![Extent {
num_sectors: 991848,
extent_type: ExtentType::Linear {
start_sector: 1761280,
block_device_index: 0,
},
}],
},
Partition {
name: "product".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![
Extent {
num_sectors: 3627008,
extent_type: ExtentType::Linear {
start_sector: 2754560,
block_device_index: 0,
},
},
Extent {
num_sectors: 538240,
extent_type: ExtentType::Linear {
start_sector: 2048,
block_device_index: 1,
},
},
],
},
Partition {
name: "system_ext".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![Extent {
num_sectors: 490744,
extent_type: ExtentType::Linear {
start_sector: 540672,
block_device_index: 1,
},
}],
},
],
},
],
block_devices: vec![
BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 3267362816,
partition_name: "system".into(),
flags: BlockDeviceFlags::SLOT_SUFFIXED,
},
BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 805306368,
partition_name: "vendor".into(),
flags: BlockDeviceFlags::SLOT_SUFFIXED,
},
],
flags: HeaderFlags::empty(),
};
let metadata = Metadata {
image_type: ImageType::Normal,
metadata_max_size: 65536,
metadata_slot_count: 2,
logical_block_size: 4096,
slots: vec![slot; 2],
};
// First 274432 bytes of system.img. Unlike the other test cases, this is
// identical to the original image because there is only one partition group
// with partitions, so the group-level ordering is the same as the global
// ordering.
let sha512 = [
0xb9, 0x97, 0xf5, 0x83, 0x39, 0x37, 0x90, 0x0a, 0xb6, 0x46, 0xdd, 0x27, 0x57, 0xf1, 0xf3,
0xbd, 0x8f, 0xc4, 0x63, 0x07, 0x6f, 0xf4, 0x19, 0xc0, 0x02, 0x28, 0x48, 0x99, 0x54, 0xbb,
0xb3, 0xbf, 0x67, 0x95, 0xc4, 0xa7, 0x99, 0xf4, 0xa9, 0xc4, 0xf4, 0x1d, 0xf7, 0x59, 0x28,
0xeb, 0xbc, 0x85, 0x46, 0xd1, 0x7d, 0x65, 0x0f, 0xbe, 0x21, 0xf6, 0xf2, 0xa2, 0x20, 0x5b,
0xda, 0xde, 0xfa, 0x50,
];
round_trip(&metadata, &sha512);
}
+171
View File
@@ -0,0 +1,171 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{Cursor, Read, Write};
use avbroot::format::sparse::{
self, Chunk, ChunkBounds, ChunkData, CrcMode, Header, SparseReader, SparseWriter,
};
#[derive(Clone, Copy)]
struct TestChunk {
chunk: Chunk,
data: &'static [u8],
}
fn round_trip(block_size: u32, crc32: u32, test_chunks: &[TestChunk], sha512: &[u8; 64]) {
let num_blocks = test_chunks.iter().map(|d| d.chunk.bounds.len()).sum();
let header = Header {
major_version: sparse::MAJOR_VERSION,
minor_version: sparse::MINOR_VERSION,
block_size,
num_blocks,
num_chunks: test_chunks.len() as u32,
crc32,
};
let writer = Cursor::new(Vec::new());
let mut sparse_writer = SparseWriter::new(writer, header).unwrap();
for test_chunk in test_chunks {
sparse_writer.start_chunk(test_chunk.chunk).unwrap();
if !test_chunk.data.is_empty() {
sparse_writer.write_all(test_chunk.data).unwrap();
}
}
let writer = sparse_writer.finish().unwrap();
let data = writer.into_inner();
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
sha512,
);
let reader = Cursor::new(&data);
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate).unwrap();
assert_eq!(sparse_reader.header(), header);
let mut test_chunks_iter = test_chunks.iter();
while let Some(chunk) = sparse_reader.next_chunk().unwrap() {
let test_chunk = test_chunks_iter.next().unwrap();
assert_eq!(chunk, test_chunk.chunk);
if !test_chunk.data.is_empty() {
let mut buf = vec![];
sparse_reader.read_to_end(&mut buf).unwrap();
assert_eq!(buf, test_chunk.data);
}
}
assert!(test_chunks_iter.next().is_none());
}
#[test]
fn round_trip_full_image() {
let block_size = 8;
let file_crc32 = 0xf6e23567;
let test_chunks = [
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 0, end: 1 },
data: ChunkData::Data,
},
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 1, end: 1 },
data: ChunkData::Crc32(0x88aa689f),
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 1, end: 2 },
data: ChunkData::Fill(0x01234567),
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 2, end: 3 },
data: ChunkData::Data,
},
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 3, end: 3 },
data: ChunkData::Crc32(0xf6e23567),
},
data: b"",
},
];
let sha512 = [
0x19, 0x5f, 0xa7, 0xdb, 0x18, 0xc6, 0xb9, 0x0e, 0xce, 0x4b, 0x4f, 0x35, 0x36, 0x79, 0x46,
0x02, 0x7a, 0x45, 0x66, 0x63, 0x0e, 0xd9, 0x76, 0x93, 0x2b, 0x88, 0xe2, 0xbc, 0x0b, 0xd9,
0x1f, 0x21, 0x51, 0x92, 0x00, 0x2e, 0xe3, 0xa2, 0xff, 0x24, 0xea, 0xef, 0x24, 0xd5, 0x24,
0xf0, 0x46, 0xf3, 0x10, 0x32, 0xf4, 0xa6, 0x3b, 0x9d, 0xcd, 0xc5, 0x57, 0xf4, 0xc0, 0xe8,
0x01, 0xe8, 0x1d, 0xb3,
];
round_trip(block_size, file_crc32, &test_chunks, &sha512);
}
#[test]
fn round_trip_partial_image() {
let block_size = 8;
let file_crc32 = 0;
let test_chunks = [
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 0, end: 1 },
data: ChunkData::Hole,
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 1, end: 2 },
data: ChunkData::Data,
},
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 2, end: 3 },
data: ChunkData::Hole,
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 3, end: 4 },
data: ChunkData::Data,
},
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 4, end: 5 },
data: ChunkData::Hole,
},
data: b"",
},
];
let sha512 = [
0xee, 0x07, 0xc5, 0x4d, 0x85, 0xee, 0x69, 0x91, 0x61, 0x07, 0x10, 0xed, 0xec, 0x13, 0x5e,
0xfb, 0xc3, 0x7d, 0xcf, 0x1f, 0x2a, 0x13, 0xf0, 0xb6, 0x85, 0xb4, 0xee, 0xe9, 0xd7, 0xa1,
0x12, 0x79, 0x14, 0x16, 0x30, 0x7a, 0x81, 0xf9, 0x4f, 0x72, 0xb2, 0xdd, 0x33, 0xbe, 0x5d,
0x55, 0x70, 0xa9, 0xe3, 0x94, 0x29, 0x40, 0x29, 0x8f, 0x35, 0x23, 0xf8, 0x78, 0x7f, 0xfe,
0xd6, 0x4b, 0x60, 0x16,
];
round_trip(block_size, file_crc32, &test_chunks, &sha512);
}
-221
View File
@@ -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
-195
View File
@@ -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,
)
+73
View File
@@ -0,0 +1,73 @@
[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",
"bzip2-1.0.6",
"GPL-3.0",
"ISC",
"MIT",
"Unicode-3.0",
"Zlib",
]
[[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"] },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/chenxiaolong/zip2",
]
+39
View File
@@ -0,0 +1,39 @@
[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.14"
rsa = { version = "0.9.6", features = ["hazmat"] }
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
toml_edit = { version = "0.23.3", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = "0.2.5"
# https://github.com/zip-rs/zip2/pull/367
# https://github.com/zip-rs/zip2/pull/368
# For getting the data offset when writing new zip entries.
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip2"
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
default-features = false
[features]
static = ["avbroot/static"]
[lints]
workspace = true
+26
View File
@@ -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
```
+177
View File
@@ -0,0 +1,177 @@
# 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.vabc]
# CoW v3 is used starting with the Google Pixel 9a.
version = "V3"
algo = { kind = "Lz4" }
[profile.pixel_v4_gki.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.kernel = true
[profile.pixel_v4_gki.partitions.init_boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.ramdisks = [["init", "first_stage"]]
[profile.pixel_v4_gki.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v4_gki.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "init_boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v4_gki.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v4_gki.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes_streaming]
original = "ef6261cd9ebea90f036e52a46160a400c5b8f6ef24ed2469c4a1e9689987aa06"
patched = "37fd353a766a7b9a339fbf51fa79c703e94640dc6a2c6310d79357aaefcc7ca1"
[profile.pixel_v4_gki.hashes_seekable]
original = "8a2c717607c10dfa5483d6f9a9f37b3d978acaf8d2ea18e36544af267943e750"
patched = "2c4734c9e1d028ee6aaf02bb416e5e173857faffd2ca067366790655147b3afa"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
[profile.pixel_v4_non_gki.vabc]
version = "V2"
algo = { kind = "Lz4" }
[profile.pixel_v4_non_gki.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.kernel = true
[profile.pixel_v4_non_gki.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v4_non_gki.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v4_non_gki.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v4_non_gki.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes_streaming]
original = "630220ef813a2b4743d1941179cc9705da86ad4805f1c52341dcb38fbce3d29e"
patched = "b725e91751fe58aed20495aecbf9b4bdc14d2799cd88dcbd58f3a3b02b3af15b"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "1afbe6867ded345d941098ee7c7fcf94a3df52c50ff96ab8f3a67b2ab957259a"
patched = "4357b977249006b101002c961916f962787315a80b8608494c6a1f0cf09cecd1"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[profile.pixel_v3.vabc]
version = "V2"
algo = { kind = "Gz" }
[profile.pixel_v3.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v3"
data.kernel = true
data.ramdisks = [["init"]]
[profile.pixel_v3.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v3.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v3.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v3.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes_streaming]
original = "9b65037343d45211e0f9706929cba34643a9c54274d1b39740c43f45974984e0"
patched = "fb23ab9616968b38b96d1e5e6a503154f89aebc1741e89a9e9dfd2c4d9946b05"
[profile.pixel_v3.hashes_seekable]
original = "e581934887dd93b8a9d9c3aa5dec1d48aa7e01bf01ac507e8c5fb256b59cbe7d"
patched = "669a826abc6d67e7e0b1def724aa7b470461087255c65663d195df1426a355f0"
# 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_streaming]
original = "f10ee15c900a474cc6bbefa705f272cef42636ea096e75563d2d78f6c4327fd1"
patched = "6929f65909037f5550a53982b71e96bdf69ab876bc5e86702c469ed601be8a9a"
[profile.pixel_v2.hashes_seekable]
original = "4e863d251b9ff6eaa1511f9c03e9bdb8919650b2e0eaf23e33892a639edafcaf"
patched = "9a103222e73df70a097281525546d25c850df2ae7a2ba715aa5dfbbba3f7972b"
+54
View File
@@ -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.
+29
View File
@@ -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-----
+54
View File
@@ -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
+101
View File
@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{ffi::OsString, path::PathBuf};
use avbroot::cli::args::LogFormat;
use clap::{Args, Parser, Subcommand, ValueEnum};
use tracing::Level;
#[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 = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
pub log_format: LogFormat,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum PassSource {
Env,
File,
}
#[derive(Debug, Parser)]
pub struct HelperCli {
/// Signature algorithm.
pub algorithm: String,
/// Public key.
#[arg(value_name = "FILE", value_parser)]
pub public_key: PathBuf,
/// Non-interactive password source.
#[arg(value_name = "SOURCE")]
pub pass_source: PassSource,
/// Non-interactive password source value.
#[arg(value_name = "VALUE", value_parser)]
pub pass_source_value: OsString,
}
+140
View File
@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use avbroot::format::payload::{CowVersion, VabcAlgo};
use serde::{Deserialize, Serialize};
use toml_edit::DocumentMut;
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Sha256Hash(
#[serde(
serialize_with = "hex::serialize",
deserialize_with = "hex::deserialize"
)]
pub [u8; 32],
);
#[derive(Clone, 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(Clone, Copy, 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(Clone, 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(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DmVerityData {
pub content: DmVerityContent,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VbmetaData {
pub deps: Vec<String>,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Data {
Boot(BootData),
DmVerity(DmVerityData),
Vbmeta(VbmetaData),
}
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hashes {
pub original: Sha256Hash,
pub patched: Sha256Hash,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Partition {
pub avb: Avb,
pub data: Data,
}
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VabcSettings {
pub version: CowVersion,
pub algo: VabcAlgo,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub vabc: Option<VabcSettings>,
pub partitions: BTreeMap<String, Partition>,
pub hashes_streaming: Hashes,
pub hashes_seekable: Hashes,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub ota_info: OtaInfo,
#[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))
}
+1440
View File
File diff suppressed because it is too large Load Diff
-1
Submodule external/avb deleted from 3210440973
-1
Submodule external/build deleted from 2014bbb8e7

Some files were not shown because too many files have changed in this diff Show More