mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a293147b1 | |||
| aea12c8d58 | |||
| 5545b0fe1b | |||
| 9c818fb165 | |||
| d174af8969 | |||
| 2cf11094de | |||
| 6f565969b7 | |||
| da2eb6b717 | |||
| 2f8d0264eb | |||
| ec1fe74900 | |||
| b3b7c7b738 | |||
| 2f1ee1ae4f | |||
| cb62996b9f | |||
| 54c06835c5 | |||
| 65a1b80da5 | |||
| 76ff1ddcda | |||
| 0a4dda14cd | |||
| bde8dbfbcd | |||
| fbafbafe90 | |||
| a14cab71e5 | |||
| c2f4297bb7 | |||
| 9d63c9759d | |||
| 73b893f3ed | |||
| fd7bbf5751 | |||
| 409867a8e5 | |||
| f96a2887df | |||
| 8140e25620 | |||
| ce05477ab1 | |||
| 1c4800c0cb | |||
| d402fdf5f1 | |||
| 4930527598 | |||
| 9b970c6d25 | |||
| e7fb4004cd | |||
| 2773dcddcf | |||
| 36765290fd | |||
| 6d8268e472 | |||
| 2e8e86766b | |||
| 8ae1c54c13 | |||
| 5212d4de51 | |||
| 61bb9d6a81 | |||
| 6d86fbf8a0 | |||
| 621ccc254c | |||
| f41ef844e0 | |||
| 42f8769e48 | |||
| d2187676d1 | |||
| 3de5194c46 | |||
| e0c1d4ad3a | |||
| c94af49009 | |||
| 5ed7dd7dca |
@@ -7,6 +7,32 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### 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.
|
||||
@@ -76,6 +102,7 @@ Behind-the-scenes changes:
|
||||
* 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
|
||||
[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
|
||||
@@ -83,6 +110,8 @@ Behind-the-scenes changes:
|
||||
[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
|
||||
[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
|
||||
@@ -117,3 +146,16 @@ Behind-the-scenes changes:
|
||||
[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
|
||||
|
||||
Generated
+101
-114
@@ -21,9 +21,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -90,9 +90,9 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
|
||||
checksum = "a2e1373abdaa212b704512ec2bd8b26bd0b7d5c3f70117411a5d9a451383c859"
|
||||
|
||||
[[package]]
|
||||
name = "assert_matches"
|
||||
@@ -121,7 +121,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "avbroot"
|
||||
version = "2.2.0"
|
||||
version = "2.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -169,9 +169,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.4"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
@@ -193,9 +193,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -217,9 +217,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.6.2"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
|
||||
checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
@@ -228,9 +228,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
@@ -334,9 +334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.4.6"
|
||||
version = "4.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
|
||||
checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -344,9 +344,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.4.6"
|
||||
version = "4.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
|
||||
checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -356,30 +356,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.4.3"
|
||||
version = "4.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ae8ba90b9d8b007efe66e55e48fb936272f5ca00349b5b0e89877520d35ea7"
|
||||
checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.4.2"
|
||||
version = "4.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
|
||||
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
|
||||
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
|
||||
|
||||
[[package]]
|
||||
name = "cms"
|
||||
@@ -423,9 +423,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.9"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
|
||||
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -548,7 +548,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -565,7 +565,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "2.2.0"
|
||||
version = "2.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
@@ -594,25 +594,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480"
|
||||
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evalexpr"
|
||||
version = "6.6.0"
|
||||
@@ -639,9 +628,9 @@ checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.27"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
|
||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -690,7 +679,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fuzz"
|
||||
version = "2.2.0"
|
||||
version = "2.3.2"
|
||||
dependencies = [
|
||||
"avbroot",
|
||||
"honggfuzz",
|
||||
@@ -742,9 +731,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.1"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12"
|
||||
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -862,9 +851,9 @@ checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c"
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
@@ -892,21 +881,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
version = "0.2.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.8"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db"
|
||||
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
@@ -934,7 +923,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex-syntax 0.6.29",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1015,7 +1004,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1057,7 +1046,7 @@ version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
@@ -1103,9 +1092,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
||||
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
@@ -1123,7 +1112,7 @@ version = "0.10.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -1140,7 +1129,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1226,7 +1215,7 @@ dependencies = [
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1295,14 +1284,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.67"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1334,7 +1323,7 @@ dependencies = [
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
"tempfile",
|
||||
"which",
|
||||
]
|
||||
@@ -1349,7 +1338,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1462,34 +1451,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.3.5"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.6"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
|
||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.7.5",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.9"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
|
||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.5",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1500,15 +1489,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.0"
|
||||
version = "0.17.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb9d44f9bf6b635117787f72416783eb7e4227aaf255e5ce739563d817176a7e"
|
||||
checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"getrandom",
|
||||
@@ -1531,16 +1520,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
|
||||
checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"const-oid",
|
||||
"digest",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
@@ -1574,11 +1561,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.15"
|
||||
version = "0.38.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"itoa",
|
||||
"libc",
|
||||
@@ -1656,35 +1643,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.19"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
|
||||
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.188"
|
||||
version = "1.0.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.188"
|
||||
version = "1.0.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
|
||||
checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1786,9 +1773,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.37"
|
||||
version = "2.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
|
||||
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1797,9 +1784,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.0"
|
||||
version = "3.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
||||
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
@@ -1810,22 +1797,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.49"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
|
||||
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.49"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
|
||||
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1845,18 +1832,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.3"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
version = "0.20.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -1939,9 +1926,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
@@ -2085,9 +2072,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.15"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
|
||||
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2098,7 +2085,7 @@ version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357bb8e2932df531f83b052264b050b81ba0df90ee5a59b2d1d3949f344f81e5"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@@ -2117,7 +2104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "2.2.0"
|
||||
version = "2.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "2.2.0"
|
||||
version = "2.3.2"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -30,6 +30,8 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
|
||||
* To force an image to be signed, use `--key <path> --force`.
|
||||
* To force an image to be unsigned, use `--force` without specifying `--key`.
|
||||
|
||||
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
|
||||
|
||||
### Repacking an AVB image
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,49 +1,112 @@
|
||||
# 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.
|
||||
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
|
||||
|
||||
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
|
||||
|
||||
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
|
||||
|
||||
* `payload.bin` exists
|
||||
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
|
||||
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders 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.
|
||||
|
||||
## Patches
|
||||
|
||||
avbroot applies two patches to the boot images:
|
||||
|
||||
* Magisk is applied to the `boot` or `init_boot` image, depending on device, as if it were done from the Magisk app.
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
|
||||
|
||||
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded after the bootloader has been locked. It also prevents accidental flashing of the original OTA package while booted into recovery.
|
||||
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded from recovery mode after the bootloader has been locked. It also prevents accidental flashing of the original unpatched OTA.
|
||||
|
||||
## Warnings and Caveats
|
||||
|
||||
* The device must use (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. To check if a device uses this partitioning sceme, open the OTA zip file and check that:
|
||||
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
|
||||
|
||||
* `payload.bin` exists
|
||||
* `META-INF/com/android/metadata.pb` exists
|
||||
* `META-INF/com/android/metadata` contains the line: `ota-type=AB`
|
||||
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
|
||||
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders support this, as well as most OnePlus devices. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
* Any operation that causes an improperly-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
|
||||
|
||||
* **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_**.
|
||||
* Performing a unpatched A/B OTA update while booted into Android via the OS' default updater. This can be blocked via a Magisk/KernelSU module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
|
||||
|
||||
* 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 `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
|
||||
|
||||
* Performing a regular (unpatched) A/B OTA update. This can be blocked via a Magisk module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
|
||||
If the boot image is ever modified, **do not reboot**. [Open an issue](https://github.com/chenxiaolong/avbroot/issues/new) for support and be very clear about what steps were done that lead to the situation. If Android is still running and root access works, it might be possible to recover without wiping and starting over.
|
||||
|
||||
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
|
||||
## Usage
|
||||
|
||||
1. Make sure the [caveats listed above](#warnings-and-caveats) are understood. It is possible to hard brick by doing the wrong thing!
|
||||
|
||||
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
|
||||
|
||||
avbroot is a standalone executable. It does not need to be installed and can be run from anywhere.
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
4. Patch the OTA zip. The base command is:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--input /path/to/ota.zip \
|
||||
--key-avb /path/to/avb.key \
|
||||
--key-ota /path/to/ota.key \
|
||||
--cert-ota /path/to/ota.crt \
|
||||
```
|
||||
|
||||
Add the following additional arguments to the end of the command depending on how you want to configure root access.
|
||||
|
||||
* To enable root access with Magisk:
|
||||
|
||||
```bash
|
||||
--magisk /path/to/magisk.apk \
|
||||
--magisk-preinit-device <name>
|
||||
```
|
||||
|
||||
If you don't know the Magisk preinit partition name, see the [Magisk preinit device section](#magisk-preinit-device) for steps on how to find it.
|
||||
|
||||
If you prefer to manually patch the boot image via the Magisk app instead of letting avbroot handle it, use the following arguments instead:
|
||||
|
||||
```bash
|
||||
--prepatched /path/to/magisk_patched-xxxxx_yyyyy.img
|
||||
```
|
||||
|
||||
* To enable root access with KernelSU:
|
||||
|
||||
```bash
|
||||
--prepatched /path/to/kernelsu/boot.img \
|
||||
--boot-partition @gki_kernel
|
||||
```
|
||||
|
||||
* To leave the OS unrooted:
|
||||
|
||||
```bash
|
||||
--rootless
|
||||
```
|
||||
|
||||
For more details on the options above, see the [advanced usage section](#advanced-usage).
|
||||
|
||||
If `--output` is not specified, then the output file is written to `<input>.patched`.
|
||||
|
||||
5. The patched OTA is ready to go! To flash it for the first time, follow the steps in the [initial setup section](#initial-setup). For updates, follow the steps in the [updates section](#updates).
|
||||
|
||||
## Generating Keys
|
||||
|
||||
avbroot signs a few components while patching an OTA zip:
|
||||
avbroot signs several components while patching an OTA zip:
|
||||
|
||||
* the root `vbmeta` image
|
||||
* the boot image `vbmeta` footers (if the original ones were signed)
|
||||
* the boot images
|
||||
* the vbmeta images
|
||||
* the OTA payload
|
||||
* the OTA zip itself
|
||||
|
||||
The boot-related components are signed with an AVB key and OTA-related components are signed with an OTA key. They can be the same RSA keypair, though the following steps show how to generate two separate keys.
|
||||
The first two components are signed with an AVB key and latter two components are signed with an OTA key. They can be the same key, though the following steps show how to generate two separate keys.
|
||||
|
||||
1. Generate the AVB and OTA signing keys:
|
||||
When patching OTAs for multiple devices, generating unique keys for each device is strongly recommended because it prevents an OTA for the wrong device being accidentally flashed.
|
||||
|
||||
1. Generate the AVB and OTA signing keys.
|
||||
|
||||
```bash
|
||||
avbroot key generate-key -o avb.key
|
||||
@@ -56,44 +119,23 @@ The boot-related components are signed with an AVB key and OTA-related component
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery for verifying OTA updates.
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
|
||||
|
||||
```bash
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and X509 certificate (eg. like those generated by openssl).
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
|
||||
|
||||
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (including a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
|
||||
|
||||
## Usage
|
||||
## Initial setup
|
||||
|
||||
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
|
||||
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
|
||||
|
||||
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.
|
||||
2. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
4. Patch the full OTA ZIP.
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--input /path/to/ota.zip \
|
||||
--privkey-avb /path/to/avb.key \
|
||||
--privkey-ota /path/to/ota.key \
|
||||
--cert-ota /path/to/ota.crt \
|
||||
--magisk /path/to/magisk.apk
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
5. **[Initial setup only]** Unlock the bootloader. This will trigger a data wipe.
|
||||
|
||||
6. **[Initial setup only]** Extract the patched images from the patched OTA.
|
||||
3. Extract the partition images from the patched OTA that are different from the original.
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
@@ -101,73 +143,108 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
7. **[Initial setup only]** Flash the patched images and the AVB public key metadata. This sets up the custom root of trust. Future updates are done by simply sideloading patched OTA zips.
|
||||
If you are using KernelSU, also add `--boot-partition @gki_kernel` to the command.
|
||||
|
||||
4. Flash the partition images that were extracted.
|
||||
|
||||
This can be done by manually running `fastboot flash <partition> extracted/<partition>.img` for each image in the `extracted/` directory or by using the following script:
|
||||
|
||||
```bash
|
||||
# Flash the boot images that were extracted
|
||||
for image in extracted/*.img; do
|
||||
partition=$(basename "${image}")
|
||||
partition=${partition%.img}
|
||||
|
||||
fastboot flash "${partition}" "${image}"
|
||||
done
|
||||
```
|
||||
|
||||
# Flash the AVB signing public key
|
||||
5. Set up the custom AVB public key in the bootloader.
|
||||
|
||||
```bash
|
||||
fastboot erase avb_custom_key
|
||||
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
|
||||
```
|
||||
|
||||
8. **[Initial setup only]** Run `dmesg | grep libfs_avb` as root to verify that AVB is working properly. A message similar to the following is expected:
|
||||
6. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
|
||||
|
||||
Install the Magisk or KernelSU app and run the following command:
|
||||
|
||||
```bash
|
||||
adb shell su -c 'dmesg | grep libfs_avb'
|
||||
```
|
||||
|
||||
If AVB is working properly, the following message should be printed out:
|
||||
|
||||
```bash
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
9. **[Initial setup only]** Lock the bootloader. This will trigger a data wipe again. **Do not uncheck `OEM unlocking`!**
|
||||
7. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
|
||||
|
||||
Remember: **Do not uncheck `OEM unlocking`!**
|
||||
|
||||
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using [avbroot's `oemunlockonboot` Magisk module](#oemunlockonboot-enable-oem-unlocking-on-every-boot) to automatically ensure OEM unlocking is enabled on every boot.
|
||||
|
||||
8. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
|
||||
|
||||
For extra safety, consider flashing [avbroot's Magisk/KernelSU modules](#avbroot-modules).
|
||||
|
||||
## Updates
|
||||
|
||||
To update Android or Magisk:
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
|
||||
|
||||
1. Follow step 4 in [the previous section](#usage) to patch the new OTA (or an existing OTA with a newer Magisk APK).
|
||||
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
|
||||
|
||||
2. Reboot to recovery mode. If stuck at a `No command` screen, press the volume up button once while holding down the power button.
|
||||
2. Follow the step in the [usage section](#usage) to patch the new OTA.
|
||||
|
||||
3. Sideload the patched OTA.
|
||||
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
|
||||
|
||||
4. Reboot.
|
||||
4. Sideload the patched OTA with `adb sideload`.
|
||||
|
||||
## avbroot Magisk modules
|
||||
5. That's it!
|
||||
|
||||
avbroot's Magisk modules can be found on the [releases page](https://github.com/chenxiaolong/avbroot/releases) or they can be built locally by running:
|
||||
## Reverting to stock firmware
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
```
|
||||
To stop using avbroot and revert to the stock firmware:
|
||||
|
||||
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
|
||||
1. Reboot into fastboot mode and unlock the bootloader. This will trigger a data wipe.
|
||||
|
||||
### `clearotacerts`: Blocking A/B OTA Updates
|
||||
2. Erase the custom AVB public key.
|
||||
|
||||
Unpatched OTA updates are already blocked in recovery because the original OTA certificate has been replaced with the custom certificate. To disable automatic OTAs while booted into Android, turn off `Automatic system updates` in Android's Developer Options.
|
||||
```bash
|
||||
fastboot erase avb_custom_key
|
||||
```
|
||||
|
||||
The `clearotacerts` module additionally makes A/B OTAs fail while booted into Android to prevent accidental manual updates. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
|
||||
3. Flash the stock firmware.
|
||||
|
||||
At least in CalyxOS, the Updater app does not respect the `Automatic system updates` setting and may enter an infinite loop downloading the OTA update and restarting the download when signature verification fails. If this happens on your ROM, you can try to either remove network access from the Updater app or disable the Updater app altogether (if your ROM allows you to do so). In CalyxOS, it is possible to go to `Settings > Apps > See all apps`, open the three-dot menu, `Show system`, then find the `System updater` app and disable it.
|
||||
4. That's it! There are no other remnants to clean up.
|
||||
|
||||
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
|
||||
## avbroot modules
|
||||
|
||||
avbroot's Magisk/KernelSU modules can be downloaded from the [releases page](https://github.com/chenxiaolong/avbroot/releases).
|
||||
|
||||
### `clearotacerts`: Block OTA Updates from default updater app
|
||||
|
||||
Unpatched OTA updates are already blocked when booted into recovery mode because the original OTA certificate has been replaced with the custom certificate. However, this doesn't prevent the Android's system updater app from attempting to install an unpatched OTA update.
|
||||
|
||||
Disabling the system updater app is recommended. 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).
|
||||
|
||||
As an extra safety measure, flashing the `clearotacerts` module will intentionally make OTAs fail to install while booted into Android. It does so by overriding `/system/etc/security/otacerts.zip` with an empty zip containing no certificates so that even if an OTA is downloaded, signature verification will fail. This may cause some custom OS' system updater app to get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails, so make sure the system updater app is disabled.
|
||||
|
||||
As an alternative to this module, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that installs updates from a self-hosted OTA server.
|
||||
|
||||
### `oemunlockonboot`: Enable OEM unlocking on every boot
|
||||
|
||||
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS's initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
|
||||
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS' initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
|
||||
|
||||
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
|
||||
|
||||
## Magisk preinit device
|
||||
|
||||
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition'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:
|
||||
|
||||
@@ -178,9 +255,9 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
--boot-only
|
||||
```
|
||||
|
||||
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device! The partition name will be incorrect if patched from Magisk on a different device.
|
||||
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
|
||||
|
||||
The Magisk app will include a line like the following in the output:
|
||||
The Magisk app will print out a line like the following in the output:
|
||||
|
||||
```
|
||||
- Pre-init storage partition device ID: <name>
|
||||
@@ -210,6 +287,8 @@ avbroot ota verify \
|
||||
--public-key-avb /path/to/avb_pkmd.bin
|
||||
```
|
||||
|
||||
This command works for any OTA, regardless if it's patched or unpatched.
|
||||
|
||||
If the `--cert-ota` and `--public-key-avb` options are omitted, then the signatures are only checked for validity, not that they are trusted.
|
||||
|
||||
## Tab completion
|
||||
@@ -252,15 +331,15 @@ Invoke-Expression (& avbroot completion -s powershell)
|
||||
|
||||
### Using a prepatched boot image
|
||||
|
||||
avbroot can replace the boot image with a prepatched image instead of applying the Magisk root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
|
||||
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, 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.
|
||||
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.
|
||||
|
||||
Note that avbroot will validate that the prepatched image is compatible with the original. If, for example, the header fields do not match or a boot image section is missing, then the patching process will abort. The checks are not foolproof, but should help protect against accidental use of the wrong boot image. To bypass a somewhat "safe" subset of the checks, use `--ignore-prepatched-compat`. To ignore all checks (strongly discouraged!), pass it in twice.
|
||||
|
||||
### Skipping root patches
|
||||
|
||||
avbroot can be used for just resigning an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
|
||||
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
|
||||
|
||||
### Replacing partitions
|
||||
|
||||
@@ -275,7 +354,7 @@ This has no impact on what patches are applied. For example, when using Magisk,
|
||||
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`.
|
||||
@@ -284,7 +363,7 @@ To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
|
||||
|
||||
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
|
||||
|
||||
* Supply the passphrases via files:
|
||||
* Supply the passphrases via files.
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
@@ -339,11 +418,19 @@ The output binary is written to `target/release/avbroot`.
|
||||
|
||||
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
|
||||
|
||||
By default, the build links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
|
||||
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
|
||||
|
||||
To build avbroot's modules from source, run:
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
```
|
||||
|
||||
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable must be set to the Android SDK path.
|
||||
|
||||
## Verifying digital signatures
|
||||
|
||||
First, save the public key to a file listing the keys to be trusted.
|
||||
First, save the public key to a file listing the keys to be trusted. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
|
||||
|
||||
```bash
|
||||
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
|
||||
|
||||
+2
-3
@@ -117,7 +117,7 @@ impl MagiskRootPatcher {
|
||||
// RULESDEVICE config option, which stored the writable block device as an
|
||||
// rdev major/minor pair, which was not consistent across reboots and was
|
||||
// replaced by PREINITDEVICE
|
||||
const VERS_SUPPORTED: &[Range<u32>] = &[25102..25207, 25211..26400];
|
||||
const VERS_SUPPORTED: &[Range<u32>] = &[25102..25207, 25211..26500];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
@@ -764,8 +764,6 @@ pub fn patch_boot(
|
||||
patcher.patch(&mut boot_image, cancel_signal)?;
|
||||
}
|
||||
|
||||
header.set_algo_for_key(key)?;
|
||||
|
||||
let mut descriptor_iter = header.descriptors.iter_mut().filter_map(|d| {
|
||||
if let Descriptor::Hash(h) = d {
|
||||
Some(h)
|
||||
@@ -794,6 +792,7 @@ pub fn patch_boot(
|
||||
}
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
}
|
||||
|
||||
|
||||
+153
-28
@@ -28,9 +28,9 @@ use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
Header,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile},
|
||||
stream::{self, PSeekFile, Reopen},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -92,6 +92,20 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Packing with insecure algorithms is intentionally not supported, so promote
|
||||
/// to a secure algorithm if needed.
|
||||
fn promote_insecure_hash_algorithm(algorithm: &str) -> &str {
|
||||
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
|
||||
const NEW_ALGORITHM: &str = "sha256";
|
||||
|
||||
if INSECURE_ALGORITHMS.contains(&algorithm) {
|
||||
warning!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
|
||||
NEW_ALGORITHM
|
||||
} else {
|
||||
algorithm
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
|
||||
/// both readable and writable.
|
||||
fn write_raw(
|
||||
@@ -145,7 +159,7 @@ fn write_raw_and_verify(
|
||||
|
||||
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
|
||||
|
||||
let result = verify_and_repair(None, raw_file.reopen(), descriptor, true, cancel_signal);
|
||||
let result = verify_and_repair(None, raw_file.reopen()?, descriptor, true, cancel_signal);
|
||||
|
||||
// Chop off the old hash tree and FEC data.
|
||||
raw_file.set_len(f.original_image_size)?;
|
||||
@@ -179,15 +193,13 @@ fn write_raw_and_update(
|
||||
|
||||
match info.header.appended_descriptor_mut()? {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
|
||||
d.image_size = image_size;
|
||||
d.update(
|
||||
|| Ok(Box::new(raw_file.reopen())),
|
||||
|| Ok(Box::new(raw_file.reopen())),
|
||||
cancel_signal,
|
||||
)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
d.update(&raw_file, &raw_file, cancel_signal)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
}
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
|
||||
d.image_size = image_size;
|
||||
raw_file.rewind()?;
|
||||
d.update(&mut raw_file, cancel_signal)
|
||||
@@ -198,6 +210,121 @@ fn write_raw_and_update(
|
||||
Ok(raw_file)
|
||||
}
|
||||
|
||||
/// Compute the kernel command line arguments to allow the kernel to set up the
|
||||
/// dm-verity block device without dm-init or userspace helpers. This is handled
|
||||
/// by init/do_mounts_dm.c in older Pixel devices (and ChromiumOS).
|
||||
fn compute_dm_verity_cmdline(descriptor: &HashTreeDescriptor) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
// Number of device mapper devices.
|
||||
result.push_str("dm=\"1");
|
||||
// Block device name.
|
||||
result.push_str(" vroot");
|
||||
// Block device UUID.
|
||||
result.push_str(" none");
|
||||
// Block device write mode.
|
||||
result.push_str(" ro");
|
||||
// Number of device mapper targets.
|
||||
result.push_str(" 1,");
|
||||
// Starting sector.
|
||||
result.push('0');
|
||||
// Sector count.
|
||||
write!(&mut result, " {}", descriptor.image_size / 512).unwrap();
|
||||
// dm-verity version.
|
||||
write!(&mut result, " verity {}", descriptor.dm_verity_version).unwrap();
|
||||
// Data block device (replaced by bootloader at runtime).
|
||||
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
|
||||
// Hash block device (replaced by bootloader at runtime).
|
||||
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
|
||||
// Data block size.
|
||||
write!(&mut result, " {}", descriptor.data_block_size).unwrap();
|
||||
// Hash block size.
|
||||
write!(&mut result, " {}", descriptor.hash_block_size).unwrap();
|
||||
// Number of data blocks.
|
||||
write!(
|
||||
&mut result,
|
||||
" {}",
|
||||
descriptor.image_size / u64::from(descriptor.data_block_size),
|
||||
)
|
||||
.unwrap();
|
||||
// Hash starting block (in units of the hash block size).
|
||||
write!(
|
||||
&mut result,
|
||||
" {}",
|
||||
descriptor.image_size / u64::from(descriptor.hash_block_size),
|
||||
)
|
||||
.unwrap();
|
||||
// Hash algorithm.
|
||||
write!(&mut result, " {}", descriptor.hash_algorithm).unwrap();
|
||||
// Root digest.
|
||||
write!(&mut result, " {}", &hex::encode(&descriptor.root_digest)).unwrap();
|
||||
// Salt.
|
||||
write!(&mut result, " {}", &hex::encode(&descriptor.salt)).unwrap();
|
||||
|
||||
// Number of optional arguments.
|
||||
let num_optional_args = if descriptor.fec_num_roots != 0 { 10 } else { 2 }
|
||||
+ u8::from(descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0);
|
||||
write!(&mut result, " {num_optional_args}").unwrap();
|
||||
|
||||
if descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0 {
|
||||
// [n + 1] Only check blocks once instead of on each access.
|
||||
result.push_str(" check_at_most_once");
|
||||
}
|
||||
|
||||
// [0] Corruption handling mode (replaced by bootloader at runtime).
|
||||
result.push_str(" $(ANDROID_VERITY_MODE)");
|
||||
// [1] Force return zeros and skip validation for blocks expected to contain
|
||||
// only zeros.
|
||||
result.push_str(" ignore_zero_blocks");
|
||||
|
||||
if descriptor.fec_num_roots != 0 {
|
||||
// [2-3] Enable FEC (replaced by bootloader at runtime).
|
||||
result.push_str(" use_fec_from_device PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
|
||||
// [4-5] Number of parity bytes per FEC codeword.
|
||||
write!(&mut result, " fec_roots {}", descriptor.fec_num_roots).unwrap();
|
||||
|
||||
let fec_block_offset = descriptor.fec_offset / u64::from(descriptor.data_block_size);
|
||||
|
||||
// [6-7] Number of data blocks covered by FEC.
|
||||
write!(&mut result, " fec_blocks {fec_block_offset}").unwrap();
|
||||
// [8-9] Starting block (in data block size units) of FEC.
|
||||
write!(&mut result, " fec_start {fec_block_offset}").unwrap();
|
||||
}
|
||||
|
||||
// Root filesystem block device.
|
||||
result.push_str("\" root=/dev/dm-0");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Update the dm-verity kernel command line descriptor to match the hash tree
|
||||
/// descriptor. This is a no-op if there's no matching existing kernel command
|
||||
/// line descriptor to update. Returns whether the descriptor was updated.
|
||||
fn update_dm_verity_cmdline(info: &mut AvbInfo) -> Result<bool> {
|
||||
assert!(info.footer.is_some(), "Not an appended image");
|
||||
|
||||
let new_cmdline = match info.header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => compute_dm_verity_cmdline(d),
|
||||
AppendedDescriptorRef::Hash(_) => return Ok(false),
|
||||
};
|
||||
|
||||
for d in &mut info.header.descriptors {
|
||||
if let Descriptor::KernelCmdline(d) = d {
|
||||
if d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
|
||||
&& d.cmdline.starts_with("dm=")
|
||||
&& d.cmdline != new_cmdline
|
||||
{
|
||||
d.cmdline = new_cmdline;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Sign or clear header signatures based on whether the original header was
|
||||
/// signed. If the original header was signed and is unchanged, then the
|
||||
/// original signature is used as-is. If the force option is specified, then
|
||||
@@ -384,7 +511,7 @@ fn verify_and_repair(
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
status!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(|| Ok(Box::new(file.reopen())), cancel_signal) {
|
||||
match d.verify(&file, cancel_signal) {
|
||||
Err(
|
||||
e @ avb::Error::InvalidRootDigest { .. }
|
||||
| e @ avb::Error::InvalidHashTree { .. },
|
||||
@@ -392,17 +519,12 @@ fn verify_and_repair(
|
||||
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warning!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(|| Ok(Box::new(file.reopen())), cancel_signal)
|
||||
.map(|_| {
|
||||
status!("Successfully repaired data{suffix}");
|
||||
})
|
||||
d.verify(&file, cancel_signal).map(|_| {
|
||||
status!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
ret => ret,
|
||||
}
|
||||
@@ -490,7 +612,11 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
format!("Failed to open raw image for reading: {:?}", cli.input_raw)
|
||||
})?;
|
||||
|
||||
write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?
|
||||
let file = write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?;
|
||||
|
||||
update_dm_verity_cmdline(&mut info)?;
|
||||
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
@@ -516,15 +642,14 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
let file = write_raw_and_verify(&cli.output, &mut reader, &info, false, cancel_signal)?;
|
||||
|
||||
// Write new hash tree and FEC data instead of copying the original.
|
||||
// THere could have been errors in the original FEC data itself.
|
||||
// There could have been errors in the original FEC data itself.
|
||||
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
|
||||
d.update(
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
cancel_signal,
|
||||
)?;
|
||||
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
|
||||
d.update(&file, &file, cancel_signal)?;
|
||||
}
|
||||
|
||||
update_dm_verity_cmdline(&mut info)?;
|
||||
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
@@ -664,7 +789,7 @@ struct UnpackCli {
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
output_raw: PathBuf,
|
||||
|
||||
// Ignore invalid digests or FEC data.
|
||||
/// Ignore invalid digests or FEC data.
|
||||
#[arg(long)]
|
||||
ignore_invalid: bool,
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ fn write_fec(path: &Path, fec: &FecImage) -> Result<()> {
|
||||
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
|
||||
let fec = FecImage::generate(|| Ok(Box::new(input.reopen())), cli.parity, cancel_signal)
|
||||
let fec = FecImage::generate(&input, cli.parity, cancel_signal)
|
||||
.context("Failed to generate FEC data")?;
|
||||
|
||||
write_fec(&cli.fec, &fec)?;
|
||||
@@ -65,7 +65,7 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
|
||||
fec.verify(|| Ok(Box::new(input.reopen())), cancel_signal)
|
||||
fec.verify(&input, cancel_signal)
|
||||
.context("Failed to verify data")?;
|
||||
|
||||
Ok(())
|
||||
@@ -78,12 +78,8 @@ fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
// 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(
|
||||
|| Ok(Box::new(input.reopen())),
|
||||
|| Ok(Box::new(input.reopen())),
|
||||
cancel_signal,
|
||||
)
|
||||
.context("Failed to repair file")?;
|
||||
fec.repair(&input, &input, cancel_signal)
|
||||
.context("Failed to repair file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+301
-129
@@ -9,7 +9,7 @@ use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
fmt::Display,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
time::Instant,
|
||||
@@ -43,8 +43,8 @@ use crate::{
|
||||
build::tools::releasetools::OtaMetadata, chromeos_update_engine::DeltaArchiveManifest,
|
||||
},
|
||||
stream::{
|
||||
self, CountingWriter, FromReader, HolePunchingWriter, PSeekFile, ReadSeek, SectionReader,
|
||||
ToWriter,
|
||||
self, CountingWriter, FromReader, HolePunchingWriter, PSeekFile, ReadSeek, ReadSeekReopen,
|
||||
Reopen, SectionReader, ToWriter,
|
||||
},
|
||||
util,
|
||||
};
|
||||
@@ -59,12 +59,19 @@ static PARTITION_PRIORITIES: phf::Map<&'static str, &[&'static str]> = phf_map!
|
||||
};
|
||||
|
||||
fn joined(into_iter: impl IntoIterator<Item = impl Display>) -> String {
|
||||
let items = into_iter
|
||||
.into_iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
use std::fmt::Write;
|
||||
|
||||
items.join(", ")
|
||||
let mut result = String::new();
|
||||
|
||||
for (i, item) in into_iter.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push_str(", ");
|
||||
}
|
||||
|
||||
write!(result, "{item}").expect("Failed to allocate");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn sorted<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
|
||||
@@ -142,7 +149,7 @@ pub fn get_required_images(
|
||||
/// in `external_images`, the real file on the filesystem is opened. Otherwise,
|
||||
/// the image is extracted from the payload.
|
||||
fn open_input_streams(
|
||||
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
required_images: &HashMap<String, String>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
header: &PayloadHeader,
|
||||
@@ -167,9 +174,8 @@ fn open_input_streams(
|
||||
} else {
|
||||
status!("Extracting from original payload: {name}");
|
||||
|
||||
let stream =
|
||||
payload::extract_image_to_memory(&open_payload, header, name, cancel_signal)
|
||||
.with_context(|| format!("Failed to extract from original payload: {name}"))?;
|
||||
let stream = payload::extract_image_to_memory(payload, header, name, cancel_signal)
|
||||
.with_context(|| format!("Failed to extract from original payload: {name}"))?;
|
||||
input_streams.insert(name.clone(), Box::new(stream));
|
||||
}
|
||||
}
|
||||
@@ -236,16 +242,14 @@ fn patch_boot_images(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// From the set of input images (modified partitions + all vbmeta partitions)
|
||||
/// and determine the order to patch the vbmeta images so that it can be done in
|
||||
/// a single pass.
|
||||
fn get_vbmeta_patch_order(
|
||||
/// Load the specified vbmeta image headers. If an image has a vbmeta footer,
|
||||
/// then an error is returned because the vbmeta patching logic only ever writes
|
||||
/// root vbmeta images.
|
||||
fn load_vbmeta_images(
|
||||
images: &mut HashMap<String, Box<dyn ReadSeek + Send>>,
|
||||
vbmeta_images: &HashSet<String>,
|
||||
) -> Result<Vec<(String, Header, HashSet<String>)>> {
|
||||
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
|
||||
let mut headers = HashMap::<&str, Header>::new();
|
||||
let mut missing = images.keys().cloned().collect::<BTreeSet<_>>();
|
||||
) -> Result<HashMap<String, Header>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for name in vbmeta_images {
|
||||
let reader = images.get_mut(name).unwrap();
|
||||
@@ -253,9 +257,83 @@ fn get_vbmeta_patch_order(
|
||||
.with_context(|| format!("Failed to load vbmeta image: {name}"))?;
|
||||
|
||||
if let Some(f) = footer {
|
||||
warning!("{name} is a vbmeta partition, but has a footer: {f:?}");
|
||||
bail!("{name} is a vbmeta partition, but has a footer: {f:?}");
|
||||
}
|
||||
|
||||
result.insert(name.clone(), header);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Check if a partition is critical to AVB's chain of trust. This is not
|
||||
/// foolproof and uses a heuristic based on AOSP's boot process. OEM-specific
|
||||
/// partitions may be equally important, but it's infeasible to list them all.
|
||||
fn is_critical_to_avb(name: &str) -> bool {
|
||||
name.ends_with("boot")
|
||||
|| name.starts_with("odm")
|
||||
|| name.starts_with("system")
|
||||
|| name.starts_with("vbmeta")
|
||||
|| name.starts_with("vendor")
|
||||
|| name == "dtbo"
|
||||
|| name == "product"
|
||||
|| name == "pvmfw"
|
||||
|| name == "recovery"
|
||||
}
|
||||
|
||||
/// Check that all critical partitions within the payload are protected by a
|
||||
/// vbmeta image in `vbmeta_headers`.
|
||||
fn ensure_partitions_protected(
|
||||
manifest: &DeltaArchiveManifest,
|
||||
vbmeta_headers: &HashMap<String, Header>,
|
||||
) -> Result<()> {
|
||||
let critical_partitions = manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.filter(|n| is_critical_to_avb(n))
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
// vbmeta partitions first.
|
||||
let mut avb_partitions = vbmeta_headers.keys().cloned().collect::<BTreeSet<_>>();
|
||||
|
||||
// Then, everything referred to by the descriptors.
|
||||
for header in vbmeta_headers.values() {
|
||||
let partition_names = header
|
||||
.descriptors
|
||||
.iter()
|
||||
.filter_map(|d| d.partition_name())
|
||||
.map(|n| n.to_owned());
|
||||
|
||||
avb_partitions.extend(partition_names);
|
||||
}
|
||||
|
||||
let missing = critical_partitions
|
||||
.difference(&avb_partitions)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !missing.is_empty() {
|
||||
bail!(
|
||||
"Found critical partitions that are not protected by AVB: {}",
|
||||
joined(missing),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// From the set of input images (modified partitions + all vbmeta partitions),
|
||||
/// determine the order to patch the vbmeta images so that it can be done in a
|
||||
/// single pass.
|
||||
fn get_vbmeta_patch_order(
|
||||
images: &mut HashMap<String, Box<dyn ReadSeek + Send>>,
|
||||
vbmeta_headers: &HashMap<String, Header>,
|
||||
) -> Result<Vec<(String, HashSet<String>)>> {
|
||||
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
|
||||
let mut missing = images.keys().cloned().collect::<BTreeSet<_>>();
|
||||
|
||||
for (name, header) in vbmeta_headers {
|
||||
dep_graph.insert(name, HashSet::new());
|
||||
missing.remove(name);
|
||||
|
||||
@@ -273,34 +351,31 @@ fn get_vbmeta_patch_order(
|
||||
missing.remove(partition_name);
|
||||
}
|
||||
}
|
||||
|
||||
headers.insert(name, header);
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
warning!("Partitions aren't protected by AVB: {:?}", joined(missing));
|
||||
}
|
||||
|
||||
// Prune vbmeta images we don't need.
|
||||
loop {
|
||||
let unneeded = dep_graph
|
||||
.iter()
|
||||
.find(|(_, d)| d.is_empty())
|
||||
.map(|(&n, _)| n.to_owned());
|
||||
match unneeded {
|
||||
Some(name) => {
|
||||
dep_graph.remove(name.as_str());
|
||||
headers.remove(name.as_str());
|
||||
// Ensure that there's only a single root of trust. Otherwise, there could
|
||||
// be eg. a `vbmeta_unused` containing all the relevant descriptors, but is
|
||||
// never loaded by the bootloader.
|
||||
let mut roots = BTreeSet::new();
|
||||
|
||||
for deps in dep_graph.values_mut() {
|
||||
deps.remove(name.as_str());
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
for name in vbmeta_headers.keys() {
|
||||
if !dep_graph.values().any(|d| d.contains(name)) {
|
||||
roots.insert(name.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the patching order. This only includes vbmeta images.
|
||||
// For zero roots, let TopologicalSort report the cycle.
|
||||
if roots.len() > 1 {
|
||||
bail!("Found multiple root vbmeta images: {}", joined(roots));
|
||||
}
|
||||
|
||||
// Compute the patching order. This only includes vbmeta images. All vbmeta
|
||||
// images are included (even those that have no dependencies) so that
|
||||
// update_vbmeta_headers() can check and update the flags field if needed.
|
||||
let mut topo = TopologicalSort::<String>::new();
|
||||
let mut order = vec![];
|
||||
|
||||
@@ -313,13 +388,9 @@ fn get_vbmeta_patch_order(
|
||||
while !topo.is_empty() {
|
||||
match topo.pop() {
|
||||
Some(item) => {
|
||||
// Only include vbmeta images that we need to modify.
|
||||
if headers.contains_key(item.as_str()) {
|
||||
order.push((
|
||||
item.clone(),
|
||||
headers.remove(item.as_str()).unwrap(),
|
||||
dep_graph.remove(item.as_str()).unwrap(),
|
||||
));
|
||||
// Only include vbmeta images.
|
||||
if dep_graph.contains_key(item.as_str()) {
|
||||
order.push((item.clone(), dep_graph.remove(item.as_str()).unwrap()));
|
||||
}
|
||||
}
|
||||
None => bail!("vbmeta dependency graph has cycle: {topo:?}"),
|
||||
@@ -329,26 +400,43 @@ fn get_vbmeta_patch_order(
|
||||
Ok(order)
|
||||
}
|
||||
|
||||
/// Update vbmeta descriptors based on the footers from the specified images and
|
||||
/// then re-sign the vbmeta images.
|
||||
fn update_vbmeta_descriptors(
|
||||
/// Update vbmeta headers.
|
||||
///
|
||||
/// * If [`Header::flags`] is non-zero, then an error is returned because the
|
||||
/// value renders AVB useless. If `clear_vbmeta_flags` is set to true, then
|
||||
/// the value is set to 0 instead.
|
||||
/// * [`Header::descriptors`] is updated for each dependency listed in `order`.
|
||||
/// * [`Header::algorithm_type`] is updated with an algorithm type that matches
|
||||
/// `key`. This is not a factor when determining if a header is changed.
|
||||
///
|
||||
/// If changes were made to a vbmeta header, then the image in `images` will be
|
||||
/// replaced with a new in-memory reader containing the new image. Otherwise,
|
||||
/// the image is removed from `images` entirely to avoid needing to repack it.
|
||||
fn update_vbmeta_headers(
|
||||
images: &mut HashMap<String, Box<dyn ReadSeek + Send>>,
|
||||
order: &mut [(String, Header, HashSet<String>)],
|
||||
headers: &mut HashMap<String, Header>,
|
||||
order: &mut [(String, HashSet<String>)],
|
||||
clear_vbmeta_flags: bool,
|
||||
key: &RsaPrivateKey,
|
||||
block_size: u64,
|
||||
) -> Result<()> {
|
||||
for (name, parent_header, deps) in order {
|
||||
let mut unchanged = HashSet::new();
|
||||
|
||||
for (name, deps) in order {
|
||||
let parent_header = headers.get_mut(name).unwrap();
|
||||
let orig_parent_header = parent_header.clone();
|
||||
|
||||
if parent_header.flags != 0 {
|
||||
if clear_vbmeta_flags {
|
||||
parent_header.flags = 0;
|
||||
} else {
|
||||
bail!("{name} header flags disable AVB {:#x}", parent_header.flags);
|
||||
bail!(
|
||||
"Verified boot is disabled by {name}'s header flags: {:#x}",
|
||||
parent_header.flags,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parent_header.set_algo_for_key(key)?;
|
||||
|
||||
for dep in deps.iter() {
|
||||
// This can't fail since the descriptor must have existed for the
|
||||
// dependency to exist.
|
||||
@@ -361,6 +449,7 @@ fn update_vbmeta_descriptors(
|
||||
let reader = images.get_mut(dep).unwrap();
|
||||
let (header, _, _) = avb::load_image(reader)
|
||||
.with_context(|| format!("Failed to load vbmeta footer from image: {dep}"))?;
|
||||
let pd_type = parent_descriptor.type_name();
|
||||
|
||||
if header.public_key.is_empty() {
|
||||
// vbmeta is unsigned. Use the existing descriptor.
|
||||
@@ -371,6 +460,7 @@ fn update_vbmeta_descriptors(
|
||||
else {
|
||||
bail!("{name} has no descriptor for itself");
|
||||
};
|
||||
let d_type = descriptor.type_name();
|
||||
|
||||
match (parent_descriptor, descriptor) {
|
||||
(Descriptor::Hash(pd), Descriptor::Hash(d)) => {
|
||||
@@ -380,7 +470,7 @@ fn update_vbmeta_descriptors(
|
||||
*pd = d.clone();
|
||||
}
|
||||
_ => {
|
||||
bail!("{name}'s descriptor for {dep} must match {dep}'s self descriptor");
|
||||
bail!("{dep} descriptor ({d_type}) does not match entry in {name} ({pd_type})");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -390,25 +480,38 @@ fn update_vbmeta_descriptors(
|
||||
d.public_key = header.public_key;
|
||||
}
|
||||
_ => {
|
||||
bail!("{name}'s descriptor for {dep} must be a chain descriptor");
|
||||
bail!("{dep} descriptor ({pd_type}) in {name} must be a chain descriptor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent_header
|
||||
.sign(key)
|
||||
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
|
||||
// Only sign and rewrite the image if we need to. Some vbmeta images may
|
||||
// have no dependencies and are only being processed to ensure that the
|
||||
// flags are set to a sane value.
|
||||
if parent_header != &orig_parent_header {
|
||||
parent_header.set_algo_for_key(key)?;
|
||||
parent_header
|
||||
.sign(key)
|
||||
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
parent_header
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write vbmeta image: {name}"))?;
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
parent_header
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write vbmeta image: {name}"))?;
|
||||
|
||||
padding::write_zeros(&mut writer, block_size)
|
||||
.with_context(|| format!("Failed to write vbmeta padding: {name}"))?;
|
||||
padding::write_zeros(&mut writer, block_size)
|
||||
.with_context(|| format!("Failed to write vbmeta padding: {name}"))?;
|
||||
|
||||
*images.get_mut(name).unwrap() = Box::new(writer);
|
||||
*images.get_mut(name).unwrap() = Box::new(writer);
|
||||
} else {
|
||||
unchanged.insert(name.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
// No need to package a replacement image if it's unchanged.
|
||||
for name in unchanged {
|
||||
images.remove(name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -445,7 +548,7 @@ fn compress_image(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn patch_ota_payload(
|
||||
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
writer: impl Write,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_partition: &str,
|
||||
@@ -456,8 +559,8 @@ fn patch_ota_payload(
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
let header =
|
||||
PayloadHeader::from_reader(open_payload()?).context("Failed to load OTA payload header")?;
|
||||
let header = PayloadHeader::from_reader(payload.reopen_boxed()?)
|
||||
.context("Failed to load OTA payload header")?;
|
||||
if !header.is_full_ota() {
|
||||
bail!("Payload is a delta OTA, not a full OTA");
|
||||
}
|
||||
@@ -499,7 +602,7 @@ fn patch_ota_payload(
|
||||
// from the old payload). The values will be replaced later if the images
|
||||
// need to be patched (eg. boot or vbmeta image).
|
||||
let mut input_streams = open_input_streams(
|
||||
&open_payload,
|
||||
payload,
|
||||
&required_images,
|
||||
external_images,
|
||||
&header_locked,
|
||||
@@ -515,11 +618,15 @@ fn patch_ota_payload(
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_streams, &vbmeta_images)?;
|
||||
let mut vbmeta_headers = load_vbmeta_images(&mut input_streams, &vbmeta_images)?;
|
||||
|
||||
ensure_partitions_protected(&header_locked.manifest, &vbmeta_headers)?;
|
||||
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_streams, &vbmeta_headers)?;
|
||||
|
||||
status!(
|
||||
"Patching vbmeta images: {}",
|
||||
joined(vbmeta_order.iter().map(|(n, _, _)| n)),
|
||||
joined(vbmeta_order.iter().map(|(n, _)| n)),
|
||||
);
|
||||
|
||||
// Get rid of input readers for vbmeta partitions we don't need to modify.
|
||||
@@ -530,8 +637,9 @@ fn patch_ota_payload(
|
||||
}
|
||||
}
|
||||
|
||||
update_vbmeta_descriptors(
|
||||
update_vbmeta_headers(
|
||||
&mut input_streams,
|
||||
&mut vbmeta_headers,
|
||||
&mut vbmeta_order,
|
||||
clear_vbmeta_flags,
|
||||
key_avb,
|
||||
@@ -559,7 +667,7 @@ fn patch_ota_payload(
|
||||
let header_locked = header.lock().unwrap();
|
||||
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
|
||||
.context("Failed to write payload header")?;
|
||||
let mut orig_payload_reader = open_payload().context("Failed to open payload")?;
|
||||
let mut orig_payload_reader = payload.reopen_boxed().context("Failed to open payload")?;
|
||||
|
||||
while payload_writer
|
||||
.begin_next_operation()
|
||||
@@ -628,12 +736,7 @@ fn patch_ota_zip(
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(OtaMetadata, u64)> {
|
||||
let mut missing = BTreeSet::from([
|
||||
ota::PATH_METADATA_PB,
|
||||
ota::PATH_OTACERT,
|
||||
ota::PATH_PAYLOAD,
|
||||
ota::PATH_PROPERTIES,
|
||||
]);
|
||||
let mut missing = BTreeSet::from([ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES]);
|
||||
|
||||
// Keep in sorted order for reproducibility and to guarantee that the
|
||||
// payload is processed before its properties file.
|
||||
@@ -648,9 +751,15 @@ fn patch_ota_zip(
|
||||
|
||||
if !missing.is_empty() {
|
||||
bail!("Missing entries in OTA zip: {:?}", joined(missing));
|
||||
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
|
||||
bail!(
|
||||
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
|
||||
ota::PATH_METADATA,
|
||||
ota::PATH_METADATA_PB,
|
||||
)
|
||||
}
|
||||
|
||||
let mut metadata_pb_raw = None;
|
||||
let mut metadata = None;
|
||||
let mut properties = None;
|
||||
let mut payload_metadata_size = None;
|
||||
let mut entries = vec![];
|
||||
@@ -671,19 +780,33 @@ fn patch_ota_zip(
|
||||
.compression_method(CompressionMethod::Stored)
|
||||
.large_file(use_zip64);
|
||||
|
||||
// Processed at the end after all other entries are written.
|
||||
match path.as_str() {
|
||||
// Convert legacy metadata from Android 11 to the modern protobuf
|
||||
// structure. Note that although we can read legacy-only OTAs, we
|
||||
// always produce both the legacy and protobuf representations in
|
||||
// the output.
|
||||
ota::PATH_METADATA => {
|
||||
// Ignore because the plain-text legacy metadata file is
|
||||
// regenerated from the new protobuf metadata.
|
||||
let mut buf = String::new();
|
||||
reader
|
||||
.read_to_string(&mut buf)
|
||||
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
|
||||
metadata = Some(
|
||||
ota::parse_legacy_metadata(&buf)
|
||||
.with_context(|| format!("Failed to parse OTA metadata: {path}"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// This takes precedence due to sorted iteration order.
|
||||
ota::PATH_METADATA_PB => {
|
||||
// Processed at the end after all other entries are written.
|
||||
let mut buf = vec![];
|
||||
reader
|
||||
.read_to_end(&mut buf)
|
||||
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
|
||||
metadata_pb_raw = Some(buf);
|
||||
metadata = Some(
|
||||
ota::parse_protobuf_metadata(&buf)
|
||||
.with_context(|| format!("Failed to parse OTA metadata: {path}"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
@@ -713,19 +836,16 @@ fn patch_ota_zip(
|
||||
bail!("{path} is not stored uncompressed");
|
||||
}
|
||||
|
||||
let payload_offset = reader.data_start();
|
||||
let payload_size = reader.size();
|
||||
// The zip library doesn't provide us with a seekable reader, so
|
||||
// we make our own from the underlying file.
|
||||
let payload_reader = SectionReader::new(
|
||||
BufReader::new(raw_reader.reopen()?),
|
||||
reader.data_start(),
|
||||
reader.size(),
|
||||
)?;
|
||||
|
||||
let (p, m) = patch_ota_payload(
|
||||
|| {
|
||||
// The zip library doesn't provide us with a seekable
|
||||
// reader, so we make our own from the underlying file.
|
||||
Ok(Box::new(SectionReader::new(
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?))
|
||||
},
|
||||
&payload_reader,
|
||||
&mut writer,
|
||||
external_images,
|
||||
boot_partition,
|
||||
@@ -778,7 +898,7 @@ fn patch_ota_zip(
|
||||
zip_writer,
|
||||
// Offset where next entry would begin.
|
||||
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
|
||||
&metadata_pb_raw.unwrap(),
|
||||
&metadata.unwrap(),
|
||||
payload_metadata_size.unwrap(),
|
||||
)
|
||||
.context("Failed to write new OTA metadata")?;
|
||||
@@ -816,18 +936,18 @@ fn extract_ota_zip(
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
|
||||
let payload_reader = SectionReader::new(
|
||||
BufReader::new(raw_reader.reopen()?),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?;
|
||||
|
||||
// Extract the images. Each time we're asked to open a new file, we just
|
||||
// clone the relevant PSeekFile. We only ever have one actual kernel file
|
||||
// descriptor for each file.
|
||||
payload::extract_images(
|
||||
|| {
|
||||
Ok(Box::new(SectionReader::new(
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?))
|
||||
},
|
||||
|name| Ok(Box::new(BufWriter::new(output_files[name].reopen()))),
|
||||
&payload_reader,
|
||||
|name| Ok(Box::new(BufWriter::new(output_files[name].reopen()?))),
|
||||
header,
|
||||
images.iter().map(|n| n.as_str()),
|
||||
cancel_signal,
|
||||
@@ -913,7 +1033,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
let raw_reader = File::open(&cli.input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()))
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
|
||||
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
|
||||
|
||||
// Open the output file for reading too, so we can verify offsets later.
|
||||
@@ -1002,7 +1122,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
let raw_reader = File::open(&cli.input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()))
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
|
||||
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
|
||||
let payload_entry = zip
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
@@ -1012,7 +1132,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
|
||||
// Open the payload data directly.
|
||||
let mut payload_reader = SectionReader::new(
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
BufReader::new(raw_reader.reopen()?),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)
|
||||
@@ -1195,19 +1315,25 @@ pub fn ota_main(cli: &OtaCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
// inside a group: https://github.com/clap-rs/clap/issues/4707. Even if that
|
||||
// were fixed, the former option's error message is much more user friendly.
|
||||
|
||||
const HEADING_PATH: &str = "Path options";
|
||||
const HEADING_KEY: &str = "Key options";
|
||||
const HEADING_MAGISK: &str = "Magisk patch options";
|
||||
const HEADING_PREPATCHED: &str = "Prepatched boot image options";
|
||||
const HEADING_OTHER: &str = "Other patch options";
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
pub struct RootGroup {
|
||||
/// Path to Magisk APK.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
#[arg(long, value_name = "FILE", value_parser, help_heading = HEADING_MAGISK)]
|
||||
pub magisk: Option<PathBuf>,
|
||||
|
||||
/// Path to prepatched boot image.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
#[arg(long, value_name = "FILE", value_parser, help_heading = HEADING_PREPATCHED)]
|
||||
pub prepatched: Option<PathBuf>,
|
||||
|
||||
/// Skip applying root patch.
|
||||
#[arg(long)]
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub rootless: bool,
|
||||
}
|
||||
|
||||
@@ -1215,23 +1341,35 @@ pub struct RootGroup {
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PatchCli {
|
||||
/// Patch to original OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
#[arg(short, long, value_name = "FILE", value_parser, help_heading = HEADING_PATH)]
|
||||
pub input: PathBuf,
|
||||
|
||||
/// Path to new OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
#[arg(short, long, value_name = "FILE", value_parser, help_heading = HEADING_PATH)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Private key for signing vbmeta images.
|
||||
#[arg(long, alias = "privkey-avb", value_name = "FILE", value_parser)]
|
||||
#[arg(
|
||||
long,
|
||||
alias = "privkey-avb",
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
help_heading = HEADING_KEY
|
||||
)]
|
||||
pub key_avb: PathBuf,
|
||||
|
||||
/// Private key for signing the OTA.
|
||||
#[arg(long, alias = "privkey-ota", value_name = "FILE", value_parser)]
|
||||
#[arg(
|
||||
long,
|
||||
alias = "privkey-ota",
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
help_heading = HEADING_KEY
|
||||
)]
|
||||
pub key_ota: PathBuf,
|
||||
|
||||
/// Certificate for OTA signing key.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
#[arg(long, value_name = "FILE", value_parser, help_heading = HEADING_KEY)]
|
||||
pub cert_ota: PathBuf,
|
||||
|
||||
/// Environment variable containing AVB private key passphrase.
|
||||
@@ -1240,7 +1378,8 @@ pub struct PatchCli {
|
||||
alias = "passphrase-avb-env-var",
|
||||
value_name = "ENV_VAR",
|
||||
value_parser,
|
||||
group = "pass_avb"
|
||||
group = "pass_avb",
|
||||
help_heading = HEADING_KEY
|
||||
)]
|
||||
pub pass_avb_env_var: Option<OsString>,
|
||||
|
||||
@@ -1250,7 +1389,8 @@ pub struct PatchCli {
|
||||
alias = "passphrase-avb-file",
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
group = "pass_avb"
|
||||
group = "pass_avb",
|
||||
help_heading = HEADING_KEY
|
||||
)]
|
||||
pub pass_avb_file: Option<PathBuf>,
|
||||
|
||||
@@ -1260,7 +1400,8 @@ pub struct PatchCli {
|
||||
alias = "passphrase-ota-env-var",
|
||||
value_name = "ENV_VAR",
|
||||
value_parser,
|
||||
group = "pass_ota"
|
||||
group = "pass_ota",
|
||||
help_heading = HEADING_KEY
|
||||
)]
|
||||
pub pass_ota_env_var: Option<OsString>,
|
||||
|
||||
@@ -1270,39 +1411,70 @@ pub struct PatchCli {
|
||||
alias = "passphrase-ota-file",
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
group = "pass_ota"
|
||||
group = "pass_ota",
|
||||
help_heading = HEADING_KEY
|
||||
)]
|
||||
pub pass_ota_file: Option<PathBuf>,
|
||||
|
||||
/// Use partition image from a file instead of the original payload.
|
||||
#[arg(long, value_names = ["PARTITION", "FILE"], value_parser = value_parser!(OsString), num_args = 2)]
|
||||
#[arg(
|
||||
long,
|
||||
value_names = ["PARTITION", "FILE"],
|
||||
value_parser = value_parser!(OsString),
|
||||
num_args = 2,
|
||||
help_heading = HEADING_PATH,
|
||||
)]
|
||||
pub replace: Vec<OsString>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub root: RootGroup,
|
||||
|
||||
/// Magisk preinit block device.
|
||||
#[arg(long, value_name = "PARTITION", conflicts_with_all = ["prepatched", "rootless"])]
|
||||
/// Magisk preinit block device (version >=25211 only).
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PARTITION",
|
||||
conflicts_with_all = ["prepatched", "rootless"],
|
||||
help_heading = HEADING_MAGISK
|
||||
)]
|
||||
pub magisk_preinit_device: Option<String>,
|
||||
|
||||
/// Magisk random seed.
|
||||
#[arg(long, value_name = "NUMBER", conflicts_with_all = ["prepatched", "rootless"])]
|
||||
/// Magisk random seed (version >=25211, <26103 only).
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "NUMBER",
|
||||
conflicts_with_all = ["prepatched", "rootless"],
|
||||
help_heading = HEADING_MAGISK
|
||||
)]
|
||||
pub magisk_random_seed: Option<u64>,
|
||||
|
||||
/// Ignore Magisk compatibility/version warnings.
|
||||
#[arg(long, conflicts_with_all = ["prepatched", "rootless"])]
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with_all = ["prepatched", "rootless"],
|
||||
help_heading = HEADING_MAGISK
|
||||
)]
|
||||
pub ignore_magisk_warnings: bool,
|
||||
|
||||
/// Ignore compatibility issues with prepatched boot images.
|
||||
#[arg(long, action = ArgAction::Count, conflicts_with_all = ["magisk", "rootless"])]
|
||||
#[arg(
|
||||
long,
|
||||
action = ArgAction::Count,
|
||||
conflicts_with_all = ["magisk", "rootless"],
|
||||
help_heading = HEADING_PREPATCHED
|
||||
)]
|
||||
pub ignore_prepatched_compat: u8,
|
||||
|
||||
/// Forcibly clear vbmeta flags if they disable AVB.
|
||||
#[arg(long)]
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub clear_vbmeta_flags: bool,
|
||||
|
||||
/// Boot partition name.
|
||||
#[arg(long, value_name = "PARTITION", default_value = "@gki_ramdisk")]
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PARTITION",
|
||||
default_value = "@gki_ramdisk",
|
||||
help_heading = HEADING_OTHER
|
||||
)]
|
||||
pub boot_partition: String,
|
||||
}
|
||||
|
||||
|
||||
+47
-26
@@ -28,8 +28,8 @@ use crate::{
|
||||
padding,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, FromReader, ReadDiscardExt, ReadSeek, ReadStringExt, ToWriter,
|
||||
WriteSeek, WriteStringExt, WriteZerosExt,
|
||||
self, CountingReader, FromReader, ReadDiscardExt, ReadSeekReopen, ReadStringExt, ToWriter,
|
||||
WriteSeekReopen, WriteStringExt, WriteZerosExt,
|
||||
},
|
||||
util,
|
||||
};
|
||||
@@ -120,8 +120,9 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
|
||||
match name {
|
||||
"sha1" if for_verify => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
|
||||
"sha256" => Ok(&ring::digest::SHA256),
|
||||
"sha512" => Ok(&ring::digest::SHA512),
|
||||
a => Err(Error::UnsupportedHashAlgorithm(a.to_owned())),
|
||||
@@ -378,6 +379,9 @@ impl fmt::Debug for HashTreeDescriptor {
|
||||
}
|
||||
|
||||
impl HashTreeDescriptor {
|
||||
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
|
||||
pub const FLAG_CHECK_AT_MOST_ONCE: u32 = 1 << 1;
|
||||
|
||||
/// 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
|
||||
@@ -427,7 +431,7 @@ impl HashTreeDescriptor {
|
||||
///
|
||||
/// NOTE: The result is **not** padded to the block size.
|
||||
fn hash_one_level_parallel(
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
block_size: u32,
|
||||
algorithm: &'static Algorithm,
|
||||
@@ -449,7 +453,7 @@ impl HashTreeDescriptor {
|
||||
let start = c * chunk_size;
|
||||
let size = chunk_size.min(image_size - start);
|
||||
|
||||
let mut reader = open_input()?;
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(start))?;
|
||||
|
||||
Self::hash_one_level(reader, size, block_size, algorithm, salt, cancel_signal)
|
||||
@@ -461,7 +465,7 @@ impl HashTreeDescriptor {
|
||||
|
||||
/// Calculate the hash tree for the given input in parallel.
|
||||
fn calculate_hash_tree(
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
image_size: u64,
|
||||
block_size: u32,
|
||||
algorithm: &'static Algorithm,
|
||||
@@ -470,7 +474,7 @@ impl HashTreeDescriptor {
|
||||
) -> io::Result<(Vec<u8>, Vec<u8>)> {
|
||||
// Small files are hashed directly, exactly like a hash descriptor.
|
||||
if image_size <= u64::from(block_size) {
|
||||
let mut reader = open_input()?;
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; block_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
@@ -500,7 +504,7 @@ impl HashTreeDescriptor {
|
||||
} else {
|
||||
// Initially read from file.
|
||||
Self::hash_one_level_parallel(
|
||||
&open_input,
|
||||
input,
|
||||
level_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
@@ -590,13 +594,13 @@ impl HashTreeDescriptor {
|
||||
/// original state by truncating it to [`Self::image_size`].
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm)?;
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, false)?;
|
||||
let (root_digest, hash_tree) = Self::calculate_hash_tree(
|
||||
&open_input,
|
||||
input,
|
||||
self.image_size,
|
||||
self.data_block_size,
|
||||
algorithm,
|
||||
@@ -610,7 +614,7 @@ impl HashTreeDescriptor {
|
||||
|
||||
let tree_size = hash_tree.len() as u64;
|
||||
|
||||
let mut writer = open_output()?;
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(self.image_size))?;
|
||||
writer
|
||||
.write_all(&hash_tree)
|
||||
@@ -633,7 +637,7 @@ impl HashTreeDescriptor {
|
||||
// The FEC covers the hash tree as well.
|
||||
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)?;
|
||||
|
||||
let fec_data = fec.generate(open_input, cancel_signal)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
let fec_size = fec_data
|
||||
.len()
|
||||
.to_u64()
|
||||
@@ -660,19 +664,19 @@ impl HashTreeDescriptor {
|
||||
/// handles to the same file.
|
||||
pub fn verify(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
self.check_offsets()?;
|
||||
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm)?;
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, true)?;
|
||||
|
||||
if self.tree_size > HASH_TREE_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("tree_size"));
|
||||
}
|
||||
|
||||
let (actual_root_digest, actual_hash_tree) = Self::calculate_hash_tree(
|
||||
&open_input,
|
||||
input,
|
||||
self.image_size,
|
||||
self.data_block_size,
|
||||
algorithm,
|
||||
@@ -687,7 +691,7 @@ impl HashTreeDescriptor {
|
||||
});
|
||||
}
|
||||
|
||||
let mut reader = open_input()?;
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(self.tree_offset))?;
|
||||
|
||||
let mut hash_tree = vec![0u8; self.tree_size as usize];
|
||||
@@ -716,7 +720,7 @@ impl HashTreeDescriptor {
|
||||
.read_exact(&mut fec_data)
|
||||
.map_err(|e| Error::ReadFieldError("fec_data", e))?;
|
||||
|
||||
fec.verify(open_input, &fec_data, cancel_signal)?;
|
||||
fec.verify(input, &fec_data, cancel_signal)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -732,8 +736,8 @@ impl HashTreeDescriptor {
|
||||
/// actually valid.
|
||||
pub fn repair(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
self.check_offsets()?;
|
||||
@@ -743,7 +747,7 @@ impl HashTreeDescriptor {
|
||||
return Err(Error::FecMissing);
|
||||
}
|
||||
|
||||
let mut reader = open_input()?;
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(self.fec_offset))?;
|
||||
|
||||
let (fec, fec_size) = self.get_fec()?;
|
||||
@@ -754,7 +758,7 @@ impl HashTreeDescriptor {
|
||||
.read_exact(&mut fec_data)
|
||||
.map_err(|e| Error::ReadFieldError("fec_data", e))?;
|
||||
|
||||
fec.repair(open_input, open_output, &fec_data, cancel_signal)?;
|
||||
fec.repair(input, output, &fec_data, cancel_signal)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -911,9 +915,10 @@ impl HashDescriptor {
|
||||
fn calculate(
|
||||
&self,
|
||||
reader: impl Read,
|
||||
for_verify: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<ring::digest::Digest> {
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm)?;
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, for_verify)?;
|
||||
let mut context = Context::new(algorithm);
|
||||
context.update(&self.salt);
|
||||
|
||||
@@ -930,14 +935,14 @@ impl HashDescriptor {
|
||||
|
||||
/// Update the root hash from the input reader's contents.
|
||||
pub fn update(&mut self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let digest = self.calculate(reader, cancel_signal)?;
|
||||
let digest = self.calculate(reader, false, cancel_signal)?;
|
||||
self.root_digest = digest.as_ref().to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the root hash against the input reader.
|
||||
pub fn verify(&self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let digest = self.calculate(reader, cancel_signal)?;
|
||||
let digest = self.calculate(reader, true, cancel_signal)?;
|
||||
|
||||
if self.root_digest != digest.as_ref() {
|
||||
return Err(Error::InvalidRootDigest {
|
||||
@@ -1051,6 +1056,11 @@ pub struct KernelCmdlineDescriptor {
|
||||
pub cmdline: String,
|
||||
}
|
||||
|
||||
impl KernelCmdlineDescriptor {
|
||||
pub const FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED: u32 = 1 << 0;
|
||||
pub const FLAG_USE_ONLY_IF_HASHTREE_DISABLED: u32 = 1 << 1;
|
||||
}
|
||||
|
||||
impl DescriptorTag for KernelCmdlineDescriptor {
|
||||
const TAG: u64 = 3;
|
||||
}
|
||||
@@ -1191,6 +1201,17 @@ pub enum Descriptor {
|
||||
}
|
||||
|
||||
impl Descriptor {
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Property(_) => "Property",
|
||||
Self::HashTree(_) => "HashTree",
|
||||
Self::Hash(_) => "Hash",
|
||||
Self::KernelCmdline(_) => "KernelCmdline",
|
||||
Self::ChainPartition(_) => "ChainPartition",
|
||||
Self::Unknown { .. } => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn partition_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::HashTree(d) => Some(&d.partition_name),
|
||||
|
||||
+22
-31
@@ -19,7 +19,7 @@ use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeek, ToWriter, WriteSeek, WriteZerosExt},
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
util::NumBytes,
|
||||
};
|
||||
|
||||
@@ -419,7 +419,7 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn generate(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let fec_size = self.fec_size();
|
||||
@@ -430,7 +430,7 @@ impl Fec {
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = open_input()?;
|
||||
let reader = input.reopen_boxed()?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -445,7 +445,7 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn verify(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -463,7 +463,7 @@ impl Fec {
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = open_input()?;
|
||||
let reader = input.reopen_boxed()?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -484,8 +484,8 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn repair(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
@@ -504,8 +504,8 @@ impl Fec {
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = open_input()?;
|
||||
let writer = open_output()?;
|
||||
let reader = input.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
self.repair_one_round(reader, writer, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
@@ -542,16 +542,16 @@ impl FecImage {
|
||||
/// Generate FEC data for a file. `parity` is the number of parity bytes per
|
||||
/// 255-byte Reed-Solomon codeword.
|
||||
pub fn generate(
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = {
|
||||
let mut file = open_input()?;
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(open_input, cancel_signal)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
fec: fec_data,
|
||||
@@ -564,11 +564,11 @@ impl FecImage {
|
||||
/// [`Self::repair()`] if performing a repair is not necessary.
|
||||
pub fn verify(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.verify(open_input, &self.fec, cancel_signal)
|
||||
fec.verify(input, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Repair a file using this instance's FEC data. The maximum correctable
|
||||
@@ -588,12 +588,12 @@ impl FecImage {
|
||||
/// that multiple threads will always read and write disjoint file offsets.
|
||||
pub fn repair(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
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(open_input, open_output, &self.fec, cancel_signal)
|
||||
fec.repair(input, output, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Build one instance of the FEC header. The caller is responsible for
|
||||
@@ -764,18 +764,15 @@ mod tests {
|
||||
let num_codewords = fec.rounds as usize * block_size as usize;
|
||||
|
||||
// Generate FEC data.
|
||||
let fec_data = fec
|
||||
.generate(|| Ok(Box::new(file.reopen())), &cancel_signal)
|
||||
.unwrap();
|
||||
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
|
||||
|
||||
// Verify that there are no errors.
|
||||
fec.verify(|| Ok(Box::new(file.reopen())), &fec_data, &cancel_signal)
|
||||
.unwrap();
|
||||
fec.verify(&file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
// Verify that errors are detected.
|
||||
corrupt_byte(&mut file, 0);
|
||||
assert_matches!(
|
||||
fec.verify(|| Ok(Box::new(file.reopen())), &fec_data, &cancel_signal,),
|
||||
fec.verify(&file, &fec_data, &cancel_signal),
|
||||
Err(Error::HasErrors)
|
||||
);
|
||||
|
||||
@@ -785,13 +782,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Verify that all the single-byte errors can be fixed.
|
||||
fec.repair(
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
&fec_data,
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
let repaired_digest = {
|
||||
let mut buf = Vec::new();
|
||||
@@ -826,7 +817,7 @@ mod tests {
|
||||
file.write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
let image = FecImage::generate(|| Ok(Box::new(file.reopen())), 2, &cancel_signal).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();
|
||||
|
||||
+117
-21
@@ -44,6 +44,8 @@ const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
|
||||
|
||||
const COMMENT_MESSAGE: &[u8] = b"signed by avbroot\0";
|
||||
|
||||
const LEGACY_SEP: &str = "|";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Cannot find OTA signature footer magic")]
|
||||
@@ -64,6 +66,10 @@ pub enum Error {
|
||||
UnsupportedDigestAlgorithm(ObjectIdentifier),
|
||||
#[error("Unsupported signature algorithm: {0}")]
|
||||
UnsupportedSignatureAlgorithm(ObjectIdentifier),
|
||||
#[error("Invalid legacy metadata line: {0:?}")]
|
||||
InvalidLegacyMetadataLine(String),
|
||||
#[error("Unsupported legacy metadata field: {key:?} = {value:?}")]
|
||||
UnsupportedLegacyMetadataField { key: String, value: String },
|
||||
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
|
||||
MismatchedPropertyFiles { expected: String, actual: String },
|
||||
#[error("Property files {0:?} exceed {1} byte reserved space")]
|
||||
@@ -92,21 +98,111 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn parse_protobuf_metadata(data: &[u8]) -> Result<OtaMetadata> {
|
||||
Ok(OtaMetadata::decode(data)?)
|
||||
}
|
||||
|
||||
/// Synthesize protobuf structure from legacy plain-text metadata.
|
||||
pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
|
||||
let mut metadata = OtaMetadata::default();
|
||||
|
||||
for line in data.split('\n') {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (key, value) = line
|
||||
.split_once('=')
|
||||
.ok_or_else(|| Error::InvalidLegacyMetadataLine(line.to_owned()))?;
|
||||
let unsupported = || Error::UnsupportedLegacyMetadataField {
|
||||
key: key.to_owned(),
|
||||
value: value.to_owned(),
|
||||
};
|
||||
// Booleans are represented by the presence or absence of `<key>=yes`.
|
||||
let parse_yes = || match value {
|
||||
"yes" => Ok(true),
|
||||
_ => Err(unsupported()),
|
||||
};
|
||||
let parse_list = || {
|
||||
value
|
||||
.split(LEGACY_SEP)
|
||||
.map(|s| s.to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
match key {
|
||||
"ota-type" => {
|
||||
match OtaType::from_str_name(value).ok_or_else(unsupported)? {
|
||||
t @ (OtaType::Ab | OtaType::Block) => metadata.set_type(t),
|
||||
// Not allowed by AOSP in the legacy format.
|
||||
_ => return Err(unsupported()),
|
||||
}
|
||||
}
|
||||
"ota-wipe" => metadata.wipe = parse_yes()?,
|
||||
"ota-retrofit-dynamic-partitions" => {
|
||||
metadata.retrofit_dynamic_partitions = parse_yes()?
|
||||
}
|
||||
"ota-downgrade" => metadata.downgrade = parse_yes()?,
|
||||
"ota-required-cache" => {
|
||||
metadata.required_cache = value.parse().map_err(|_| unsupported())?;
|
||||
}
|
||||
"post-build" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.build = parse_list();
|
||||
}
|
||||
"post-build-incremental" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.build_incremental = value.to_owned();
|
||||
}
|
||||
"post-sdk-level" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.sdk_level = value.to_owned();
|
||||
}
|
||||
"post-security-patch-level" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.security_patch_level = value.to_owned();
|
||||
}
|
||||
"post-timestamp" => {
|
||||
let p = metadata.postcondition.get_or_insert_with(Default::default);
|
||||
p.timestamp = value.parse().map_err(|_| unsupported())?;
|
||||
}
|
||||
"pre-device" => {
|
||||
let p = metadata.precondition.get_or_insert_with(Default::default);
|
||||
p.device = parse_list();
|
||||
}
|
||||
"pre-build" => {
|
||||
let p = metadata.precondition.get_or_insert_with(Default::default);
|
||||
p.build = parse_list();
|
||||
}
|
||||
"pre-build-incremental" => {
|
||||
let p = metadata.precondition.get_or_insert_with(Default::default);
|
||||
p.build_incremental = value.to_owned();
|
||||
}
|
||||
"spl-downgrade" => metadata.spl_downgrade = parse_yes()?,
|
||||
k if k.ends_with("-property-files") => {
|
||||
metadata
|
||||
.property_files
|
||||
.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
_ => {
|
||||
// Ignore. Some OEMs insert values that aren't defined in AOSP.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Generate the legacy plain-text and modern protobuf serializations of the
|
||||
/// given metadata instance.
|
||||
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
const SEP: &str = "|";
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut pairs = BTreeMap::<String, String>::new();
|
||||
|
||||
match metadata.r#type() {
|
||||
OtaType::Ab => {
|
||||
pairs.insert("ota-type".to_owned(), "AB".to_owned());
|
||||
}
|
||||
OtaType::Block => {
|
||||
pairs.insert("ota-type".to_owned(), "BLOCK".to_owned());
|
||||
}
|
||||
_ => {}
|
||||
// Other types are not allowed by AOSP in the legacy format.
|
||||
if let t @ (OtaType::Ab | OtaType::Block) = metadata.r#type() {
|
||||
pairs.insert("ota-type".to_owned(), t.as_str_name().to_owned());
|
||||
}
|
||||
if metadata.wipe {
|
||||
pairs.insert("ota-wipe".to_owned(), "yes".to_owned());
|
||||
@@ -127,7 +223,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
);
|
||||
|
||||
if let Some(p) = &metadata.postcondition {
|
||||
pairs.insert("post-build".to_owned(), p.build.join(SEP));
|
||||
pairs.insert("post-build".to_owned(), p.build.join(LEGACY_SEP));
|
||||
pairs.insert(
|
||||
"post-build-incremental".to_owned(),
|
||||
p.build_incremental.clone(),
|
||||
@@ -141,9 +237,9 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
}
|
||||
|
||||
if let Some(p) = &metadata.precondition {
|
||||
pairs.insert("pre-device".to_owned(), p.device.join(SEP));
|
||||
pairs.insert("pre-device".to_owned(), p.device.join(LEGACY_SEP));
|
||||
if !p.build.is_empty() {
|
||||
pairs.insert("pre-build".to_owned(), p.build.join(SEP));
|
||||
pairs.insert("pre-build".to_owned(), p.build.join(LEGACY_SEP));
|
||||
pairs.insert(
|
||||
"pre-build-incremental".to_owned(),
|
||||
p.build_incremental.clone(),
|
||||
@@ -157,10 +253,10 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
|
||||
pairs.extend(metadata.property_files.clone());
|
||||
|
||||
let legacy_metadata = pairs
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{k}={v}\n"))
|
||||
.collect::<String>();
|
||||
let legacy_metadata = pairs.into_iter().fold(String::new(), |mut output, (k, v)| {
|
||||
let _ = writeln!(output, "{k}={v}");
|
||||
output
|
||||
});
|
||||
let modern_metadata = metadata.encode_to_vec();
|
||||
|
||||
Ok((legacy_metadata, modern_metadata))
|
||||
@@ -286,9 +382,9 @@ fn add_payload_metadata_entry(
|
||||
/// Add metadata files to the output OTA zip. `zip_entries` is the list of
|
||||
/// [`ZipEntry`] already written to `zip_writer`. `next_offset` is the current
|
||||
/// file offset (where the next zip entry's local header begins).
|
||||
/// `metadata_pb_raw` is the serialized OTA metadata protobuf message from the
|
||||
/// original OTA. `payload_metadata_size` is the size of the new payload's
|
||||
/// metadata and metadata signature regions.
|
||||
/// `metadata` is the OTA metadata protobuf message from the original OTA.
|
||||
/// `payload_metadata_size` is the size of the new payload's metadata and
|
||||
/// metadata signature regions.
|
||||
///
|
||||
/// The zip file's backing file position MUST BE set to where the central
|
||||
/// directory would start.
|
||||
@@ -296,10 +392,10 @@ pub fn add_metadata(
|
||||
zip_entries: &[ZipEntry],
|
||||
zip_writer: &mut ZipWriter<impl Write>,
|
||||
next_offset: u64,
|
||||
metadata_pb_raw: &[u8],
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<OtaMetadata> {
|
||||
let mut metadata = OtaMetadata::decode(metadata_pb_raw)?;
|
||||
let mut metadata = metadata.clone();
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
|
||||
let mut zip_entries = zip_entries.to_owned();
|
||||
|
||||
@@ -34,15 +34,15 @@ use crate::{
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, HashingWriter, ReadDiscardExt, ReadSeek,
|
||||
SharedCursor, WriteSeek,
|
||||
self, CountingReader, CountingWriter, FromReader, HashingWriter, ReadDiscardExt,
|
||||
ReadSeekReopen, Reopen, SharedCursor, WriteSeek,
|
||||
},
|
||||
};
|
||||
|
||||
const OTA_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 1024 * 1024;
|
||||
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
@@ -871,7 +871,7 @@ pub fn apply_operation(
|
||||
/// multithreaded and uses rayon's global thread pool. `open_payload` will be
|
||||
/// called from multiple threads.
|
||||
pub fn extract_image_to_memory(
|
||||
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
header: &PayloadHeader,
|
||||
partition_name: &str,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -888,8 +888,8 @@ pub fn extract_image_to_memory(
|
||||
.operations
|
||||
.par_iter()
|
||||
.map(|op| -> Result<()> {
|
||||
let reader = open_payload()?;
|
||||
let writer = stream.reopen();
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = stream.reopen()?;
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
@@ -911,7 +911,7 @@ pub fn extract_image_to_memory(
|
||||
/// is done multithreaded and uses rayon's global thread pool. `open_payload`
|
||||
/// and `open_output` will be called from multiple threads.
|
||||
pub fn extract_images<'a>(
|
||||
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
header: &PayloadHeader,
|
||||
partition_names: impl IntoIterator<Item = &'a str>,
|
||||
@@ -938,7 +938,7 @@ pub fn extract_images<'a>(
|
||||
operations
|
||||
.into_par_iter()
|
||||
.map(|(name, op)| -> Result<()> {
|
||||
let reader = open_payload()?;
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = open_output(name)?;
|
||||
|
||||
apply_operation(
|
||||
|
||||
+70
-17
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
@@ -30,6 +30,28 @@ pub trait WriteSeek: Write + Seek {}
|
||||
|
||||
impl<W: Write + Seek> WriteSeek for W {}
|
||||
|
||||
/// A trait for seekable and reopenable readers.
|
||||
pub trait ReadSeekReopen: ReadSeek {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>>;
|
||||
}
|
||||
|
||||
impl<R: ReadSeek + Reopen + 'static> ReadSeekReopen for R {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>> {
|
||||
Ok(Box::new(self.reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for seekable and reopenable writers.
|
||||
pub trait WriteSeekReopen: WriteSeek {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>>;
|
||||
}
|
||||
|
||||
impl<W: WriteSeek + Reopen + 'static> WriteSeekReopen for W {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>> {
|
||||
Ok(Box::new(self.reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Common function for reading a structure from a reader.
|
||||
pub trait FromReader<R: Read>: Sized {
|
||||
type Error;
|
||||
@@ -168,6 +190,25 @@ impl<W: Write> WriteStringExt for W {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for file-like types to reopen themselves.
|
||||
pub trait Reopen: Sized {
|
||||
/// Open a new handle to the same file. The new handle is independently
|
||||
/// seekable and the file offset is initially set to 0.
|
||||
fn reopen(&self) -> io::Result<Self>;
|
||||
}
|
||||
|
||||
impl<R: Read + Reopen> Reopen for BufReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufReader::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Reopen> Reopen for BufWriter<W> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufWriter::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// A reader wrapper that implements [`Seek`], but only for reporting the
|
||||
/// current file position.
|
||||
pub struct CountingReader<R: Read> {
|
||||
@@ -325,6 +366,14 @@ impl<R: Read + Seek> SectionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek + Reopen> Reopen for SectionReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
let inner = self.inner.reopen()?;
|
||||
|
||||
Self::new(inner, self.start, self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for SectionReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let to_read = self.size.saturating_sub(self.pos).min(buf.len() as u64) as usize;
|
||||
@@ -417,13 +466,6 @@ impl PSeekFile {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reopen(&self) -> Self {
|
||||
Self {
|
||||
file: self.file.clone(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_len(&self, size: u64) -> io::Result<()> {
|
||||
let file_locked = self.file.read().unwrap();
|
||||
file_locked.set_len(size)
|
||||
@@ -458,6 +500,15 @@ impl PSeekFile {
|
||||
}
|
||||
}
|
||||
|
||||
impl Reopen for PSeekFile {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
file: self.file.clone(),
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for PSeekFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let n = self.read_at(buf)?;
|
||||
@@ -530,12 +581,14 @@ impl SharedCursor {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reopen(&self) -> Self {
|
||||
Self {
|
||||
impl Reopen for SharedCursor {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: self.inner.clone(),
|
||||
offset: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,8 +721,8 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, HolePunchingWriter,
|
||||
PSeekFile, ReadDiscardExt, ReadStringExt, SectionReader, SharedCursor, WriteStringExt,
|
||||
WriteZerosExt,
|
||||
PSeekFile, ReadDiscardExt, ReadStringExt, Reopen, SectionReader, SharedCursor,
|
||||
WriteStringExt, WriteZerosExt,
|
||||
};
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
@@ -848,8 +901,8 @@ mod tests {
|
||||
fn pseek_file() {
|
||||
let raw_file = tempfile::tempfile().unwrap();
|
||||
let mut a = PSeekFile::new(raw_file);
|
||||
let mut b = a.reopen();
|
||||
let mut c = b.reopen();
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
@@ -868,8 +921,8 @@ mod tests {
|
||||
#[test]
|
||||
fn shared_cursor() {
|
||||
let mut a = SharedCursor::default();
|
||||
let mut b = a.reopen();
|
||||
let mut c = b.reopen();
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
|
||||
+2
-10
@@ -138,10 +138,7 @@ fn round_trip_appended_hash_tree_image() {
|
||||
|
||||
// Verify the hash tree and FEC data.
|
||||
match header.appended_descriptor().unwrap() {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
d.verify(|| Ok(Box::new(reader.reopen())), &cancel_signal)
|
||||
.unwrap();
|
||||
}
|
||||
AppendedDescriptorRef::HashTree(d) => d.verify(&reader, &cancel_signal).unwrap(),
|
||||
AppendedDescriptorRef::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
@@ -166,12 +163,7 @@ fn round_trip_appended_hash_tree_image() {
|
||||
d.fec_offset = 0;
|
||||
d.fec_size = 0;
|
||||
|
||||
d.update(
|
||||
|| Ok(Box::new(writer.reopen())),
|
||||
|| Ok(Box::new(writer.reopen())),
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
d.update(&writer, &writer, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
+12
-12
@@ -15,13 +15,13 @@ sections = [
|
||||
]
|
||||
hash.original.full = "6b881553f012d582080642d660e1cf5c9e6fe41e9f1c6ab12ae87fab7894e307"
|
||||
hash.original.stripped = "9befd7887a125ebd8e9ae0555469dababe6bc04b0aa41aa2562036782a6d87e0"
|
||||
hash.patched.full = "6a08ae5c08e42b29ffc476511b6c60de8b77ba3f06c469a0a640e84cb95b9db9"
|
||||
hash.patched.stripped = "479b4e203d432148baaf4eaa69205c2291ec10e8541c9a78e6d9b799e994f1e3"
|
||||
hash.patched.full = "91e15447ade648c10bce599e75569ce55edc18798b11a704586acf3b51ae7971"
|
||||
hash.patched.stripped = "84b069ac7f20115ac0b19d7e319bb86d00b9f3f64e0c7a51612f99d6cae297bb"
|
||||
hash.avb_images."init_boot.img" = "3bedb41be98c46241f11219021dfbb799a6d5c89e6e00d45a66f9a5b42e7dfbc"
|
||||
hash.avb_images."vbmeta.img" = "e8e6e898ca73807edb43af0a0e86d4a94b14256def89581970287a9b1bf7a3ee"
|
||||
hash.avb_images."vbmeta_system.img" = "dbb63e08f26f46ccda501d99058d513ff71e3d6302c14d587442b666ff08862a"
|
||||
hash.avb_images."vbmeta_vendor.img" = "6ffa0a10e72c3371653be80de1380832b4d7f8bbf38a2bd861d44a4097a57117"
|
||||
hash.avb_images."vendor_boot.img" = "8f9407435e1ea532e55be418d58e1b3a4f8e0794e3cf4e562e0a0414058c6108"
|
||||
hash.avb_images."vendor_boot.img" = "dd58e8d46dd26198edf14f72d17a3315ff4c2aeb65b98dbd06369ca2a6e365a3"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -36,13 +36,13 @@ sections = [
|
||||
]
|
||||
hash.original.full = "1f1f0abe67a6f6f47287be6dafec2c12628de6a715b82ca7beddaf67ad22aca5"
|
||||
hash.original.stripped = "38b15f5efdc7e056bc799859ba72ef9a73e93c61292c59f85fb4b9c31acc5f82"
|
||||
hash.patched.full = "20f3eb522c45f8185c8c121ef2f6a18214345b85f3021d05841e1b002d348b3e"
|
||||
hash.patched.stripped = "105f4305f886a79c03fcabd296a44ef498e2b9e602970c0ef6d8599b74a704b0"
|
||||
hash.patched.full = "65f7e29591fb48ad9c7c3233df4d76bb6986aef96b94c06b73213477bc7486d8"
|
||||
hash.patched.stripped = "cf77b5e307ce4d2a62e742cd3a300964de6c693880cbfa90dcd0bfec66bd1976"
|
||||
hash.avb_images."boot.img" = "a19cb4d4fcc7f3e7d3046c3d19e2f243fb02513ca848ff92ad70d1ada55c4e65"
|
||||
hash.avb_images."vbmeta.img" = "2d817e35f7b6cdc2edce58ef249a966fc677085b70a4aced4c829320aeef0be2"
|
||||
hash.avb_images."vbmeta_system.img" = "98a050f0d53a016fbb78147b1b4a9bca3fde615aa4da34bf62c2e07a395104b5"
|
||||
hash.avb_images."vbmeta_vendor.img" = "fac530f47f237e76f3c7c3cdfe96308170dd8e8f0b227d81114a489c69ba763c"
|
||||
hash.avb_images."vendor_boot.img" = "6b66d8ea772dafc85e8745e88c9e05ff0401d062010c4d1ae0914b93cf3510aa"
|
||||
hash.avb_images."vendor_boot.img" = "b3c596360f38cd0d6212341571acf3c2d977921f8bf163d7da27978a353da9f7"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
@@ -57,12 +57,12 @@ sections = [
|
||||
]
|
||||
hash.original.full = "6d107ffac1cd3da2c972112acc75957ed725e5c13d57ca724d9bcca5404fcebd"
|
||||
hash.original.stripped = "5b889bdab3bb12ddcd3c243a56e1c58bedada8831069f49d56fe5098fb141e35"
|
||||
hash.patched.full = "c2866d8959fea55884de6281403e7daf435cded62477181354eb76332446af28"
|
||||
hash.patched.stripped = "a9ccefe44818560144f073bef8ae1c2c840cf8fff9dafb0e589bd5fd40cf0fea"
|
||||
hash.patched.full = "8725e03798539070d7075a07c80fb1403652a2b446d5c460d636a32b7368c9a2"
|
||||
hash.patched.stripped = "c4b5cfa84dc8c15f3ac9661d768062546fa36ca82cdeb6dd943347be0422903c"
|
||||
hash.avb_images."boot.img" = "8e7278a2e8ae44ffc5475717eb0e1aa56bfb7650aef34375b0fe92f790835f95"
|
||||
hash.avb_images."vbmeta.img" = "b036132b867f52a86eef79716261f35b1eb50e843dc4fe42f72cce67b24ae2db"
|
||||
hash.avb_images."vbmeta_system.img" = "9a7c6fd654e7a92aeffbdbd55ea0d87eee36f4c235e1b505423ad8a13a751a00"
|
||||
hash.avb_images."vendor_boot.img" = "e774aa770fd9c0d19a509f212309f20b08da411f8830a7383646a65571437933"
|
||||
hash.avb_images."vendor_boot.img" = "6e83d22371af4a26aef2c64cc4235f83b03978b91aef69c34eec1985e9942139"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -76,9 +76,9 @@ sections = [
|
||||
]
|
||||
hash.original.full = "01fd34b206152a3559039161c9874ab03df37da4268b86a9e0be899de5fc0af7"
|
||||
hash.original.stripped = "cc311b5bd46e06cfdefbade794d33aa9bc3ceda4ad4f38bfe9f0dfc17033d207"
|
||||
hash.patched.full = "b788d2d86008a3923734a40fb89ca98533116e51e10f35c0447beb956b236bf5"
|
||||
hash.patched.stripped = "b174f530d63bfe753ab9435a910b01a1abb2e08a8628b50101f876ed2313e536"
|
||||
hash.avb_images."boot.img" = "7cdb8e4a9b79d9c2b4d5a513c219082c0609cf6cab8cf07e122dcaaa2bd1ed63"
|
||||
hash.patched.full = "f2ac798b31a94dc251ca4ce370ebfb3073170d4a34431829df9ed0742149ffe7"
|
||||
hash.patched.stripped = "4387a5ba30c925f56c67eeaf6512757d5c1e07dfd2a8a99be14e06db8ba2dde7"
|
||||
hash.avb_images."boot.img" = "506a955080b6cfa2039ef85923e8a4e717ef6c1dc478538599c7ba26ee21e525"
|
||||
hash.avb_images."vbmeta.img" = "3679c7224e3e3e0793b4d1a031e116098460a6b4c5f3f88d5a3a0a4c65b21582"
|
||||
hash.avb_images."vbmeta_system.img" = "1d3efa00fd1d44a594c7317072468fa95c23d83d2759d6d6e757783ceeabc594"
|
||||
|
||||
|
||||
+3
-2
@@ -18,7 +18,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use avbroot::stream::PSeekFile;
|
||||
use avbroot::stream::{PSeekFile, Reopen};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Minimum download chunk size per task.
|
||||
@@ -309,7 +309,8 @@ fn download_ranges(
|
||||
}
|
||||
|
||||
if let Some(thread_range) = remaining.pop_front() {
|
||||
let file_cloned = file.reopen();
|
||||
// PSeekFile's reopen can't fail.
|
||||
let file_cloned = file.reopen().unwrap();
|
||||
let thread_range_cloned = thread_range.clone();
|
||||
let tx_cloned = tx.clone();
|
||||
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ use avbroot::{
|
||||
cli::ota::{ExtractCli, PatchCli, VerifyCli},
|
||||
format::{ota, payload::PayloadHeader},
|
||||
protobuf::chromeos_update_engine::install_operation::Type,
|
||||
stream::{self, FromReader, HashingReader, PSeekFile, SectionReader},
|
||||
stream::{self, FromReader, HashingReader, PSeekFile, Reopen, SectionReader},
|
||||
};
|
||||
use clap::Parser;
|
||||
use tempfile::TempDir;
|
||||
@@ -98,7 +98,7 @@ fn strip_image(
|
||||
let mut raw_reader = File::open(input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for reading: {input:?}"))?;
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()))
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
|
||||
.with_context(|| format!("Failed to read zip: {input:?}"))?;
|
||||
let payload_entry = zip_reader
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
@@ -108,7 +108,7 @@ fn strip_image(
|
||||
|
||||
// Open the payload data directly.
|
||||
let mut payload_reader = SectionReader::new(
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
BufReader::new(raw_reader.reopen()?),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?;
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ mod fuzz {
|
||||
input.write_zeros_exact(fec.data_size).unwrap();
|
||||
}
|
||||
|
||||
let _ = fec.verify(|| Ok(Box::new(input.reopen())), &cancel_signal);
|
||||
let _ = fec.verify(&input, &cancel_signal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.clearotacerts
|
||||
name=clearotacerts
|
||||
version=v2.2.0
|
||||
versionCode=131584
|
||||
version=v2.3.2
|
||||
versionCode=131842
|
||||
author=chenxiaolong
|
||||
description=Block A/B OTAs by clearing verification certificates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.oemunlockonboot
|
||||
name=oemunlockonboot
|
||||
version=v2.2.0
|
||||
versionCode=131584
|
||||
version=v2.3.2
|
||||
versionCode=131842
|
||||
author=chenxiaolong
|
||||
description=Enable OEM unlocking on every boot
|
||||
|
||||
@@ -50,7 +50,7 @@ fn check_brackets(line: &str) -> Result<()> {
|
||||
|
||||
fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
let re_standalone_link = Regex::new(r"\[([^\]]+)\]($|[^\(\[])")?;
|
||||
let re_auto_link = Regex::new(r"^(Issue|PR) #([0-9]+)?$")?;
|
||||
let re_auto_link = Regex::new(r"^(Discussion|Issue|PR) #([0-9]+)?$")?;
|
||||
let mut links = BTreeMap::<LinkRef, String>::new();
|
||||
|
||||
let raw_reader = File::open(path)?;
|
||||
@@ -82,6 +82,7 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
let number: u32 = captures.get(2).unwrap().as_str().parse()?;
|
||||
|
||||
let link = match link_type {
|
||||
"Discussion" => format!("{base_url}/discussions/{number}"),
|
||||
"Issue" => format!("{base_url}/issues/{number}"),
|
||||
"PR" => format!("{base_url}/pull/{number}"),
|
||||
t => bail!("Unknown link type in {link_ref:?}: {t:?}"),
|
||||
|
||||
@@ -20,7 +20,7 @@ fn update_cargo_version(version: &str) -> Result<()> {
|
||||
let data = fs::read_to_string(&path)?;
|
||||
|
||||
let mut document: Document = data.parse()?;
|
||||
document["workspace"]["package"]["version"] = value(version.clone());
|
||||
document["workspace"]["package"]["version"] = value(version);
|
||||
|
||||
fs::write(path, document.to_string())?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user