mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da124e4e05 | |||
| 7b778515d1 | |||
| 98745afe33 | |||
| a47c501211 | |||
| f2e47a65d4 | |||
| e11ddd2ba7 | |||
| 5d66774d13 | |||
| 3f09a506a0 | |||
| 4664f8ea37 | |||
| 796e2a4fa2 | |||
| e6b60d5d0f | |||
| e8cb4a8d53 | |||
| b2d280eb20 | |||
| e2dc5174b4 | |||
| 80c47e9a02 | |||
| 65ba3ad5cc | |||
| a7438876ce | |||
| c039901a85 | |||
| 0cdc7172dd | |||
| 1296275418 | |||
| a62d0a5c91 | |||
| de433a2724 | |||
| c08af33343 | |||
| e224884591 | |||
| a525fc4550 | |||
| 8c6b0fdfc5 | |||
| 36d4ed19ad | |||
| f9b06b33e6 | |||
| 620c873be5 | |||
| e929ecbe44 | |||
| 31685713ef | |||
| 0dab7aa32c | |||
| ba6c1b1400 | |||
| 1ecbf1144d | |||
| 84fa6c6bc6 | |||
| 15b7db4631 | |||
| 1d9c1574da | |||
| 43db728b9d | |||
| fbe9f629ab | |||
| 80d5f19223 | |||
| cff5ac6b2e | |||
| 2e3b5db9fe | |||
| 58a279f3a4 | |||
| 1484cd47c3 | |||
| 2ee38b716b | |||
| d4eb231dd4 |
@@ -34,8 +34,7 @@ jobs:
|
||||
- aarch64-apple-darwin
|
||||
- x86_64-apple-darwin
|
||||
combine: lipo
|
||||
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
|
||||
- os: ubuntu-24.04
|
||||
- os: ubuntu-latest
|
||||
name: aarch64-linux-android31
|
||||
targets:
|
||||
- aarch64-linux-android
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
run: |
|
||||
cargo install \
|
||||
--git https://github.com/chenxiaolong/cargo-android \
|
||||
--tag v0.1.1
|
||||
--tag v0.1.3
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
@@ -84,7 +83,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
|
||||
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
@@ -155,7 +154,7 @@ jobs:
|
||||
run: cp LICENSE README.md target/output/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@e2f4ede4a4e60ea15ff31bc0647485d80c66cfba # v2.0.4
|
||||
uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -7,6 +7,44 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.15.0
|
||||
|
||||
* Add support for changing the virtual A/B compression algorithm ([PR #437])
|
||||
* For devices that launched with Android <14, `--vabc-algo lz4` can significantly increase OTA installation speed when using a custom OTA updater app (with caveats). There is no difference when sideloading from recovery mode.
|
||||
* See [the documentation](#changing-virtual-ab-cow-compression-algorithm) for more details.
|
||||
* Switch back to the ring library now that it is maintained again ([PR #438])
|
||||
* Update dependencies ([PR #439])
|
||||
|
||||
### Version 3.14.0
|
||||
|
||||
* Report as many errors as possible before failing in `avbroot ota verify` and improve error messages ([Discussion #426], [PR #428], [PR #430])
|
||||
* Fix new clippy warnings introduced in Rust 1.85 ([PR #429])
|
||||
* Fix massive performance regression introduced in 3.13.0 for OTAs that use gzip for virtual A/B CoW compression ([Issue #433], [PR #434])
|
||||
* Update dependencies ([PR #435])
|
||||
|
||||
### Version 3.13.0
|
||||
|
||||
* Fix parsing Samsung `super.img` files in `avbroot lp` due to Samsung putting their own data structures in a region that's supposed to be filled with zeros ([PR #415])
|
||||
* Add advanced option to skip replacing the OTA certificate in the system image ([Discussion #417], [PR #418])
|
||||
* Switch to stable bzip2-rs release and use zlib-rs as the backend for flate2 ([PR #421])
|
||||
* Switch to the aws-lc cryptography library for SHA1 and SHA2 hashing ([PR #422])
|
||||
* The ring library is no longer maintained
|
||||
* Fix incorrect `Partitions aren't protected by AVB: system` warning when using `--skip-system-ota-cert` ([PR #423])
|
||||
* Discard unneeded temp file sooner when using `--skip-system-ota-cert` ([PR #424])
|
||||
* Make `avbroot lp`'s parser less strict so that it can load on-device `super` partitions ([PR #425])
|
||||
* The on-disk layout on virtual A/B devices violates some requirements stated in AOSP's documentation
|
||||
* Update dependencies ([PR #427])
|
||||
|
||||
### Version 3.12.0
|
||||
|
||||
* Add new `-p <name>` option to `avbroot ota extract` for extracting specific partitions ([PR #408])
|
||||
* Deprecate the `--boot-only` option in `avbroot ota extract` ([PR #408])
|
||||
* The option will remain indefinitely for backwards compatibility, but is hidden from `--help`
|
||||
* Add support for extracting the embedded OTA certificate and AVB public key in `avbroot ota extract` ([PR #409])
|
||||
* Rename `avbroot key extract-avb` to `avbroot key encode-avb` for consistency with `avbroot key decode-avb` ([PR #410])
|
||||
* The old syntax will remain supported indefinitely for backwards compatibility, but is hidden from `--help`
|
||||
* Update dependencies ([PR #411])
|
||||
|
||||
### Version 3.11.0
|
||||
|
||||
* Fix crash when ignoring warning about `--magisk-preinit-device` not being specified ([PR #394])
|
||||
@@ -270,6 +308,8 @@ Behind-the-scenes changes:
|
||||
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
|
||||
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
|
||||
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
|
||||
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
|
||||
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
|
||||
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
|
||||
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
|
||||
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
|
||||
@@ -296,6 +336,7 @@ Behind-the-scenes changes:
|
||||
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
|
||||
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
|
||||
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
|
||||
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
|
||||
[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
|
||||
@@ -430,3 +471,23 @@ Behind-the-scenes changes:
|
||||
[PR #402]: https://github.com/chenxiaolong/avbroot/pull/402
|
||||
[PR #403]: https://github.com/chenxiaolong/avbroot/pull/403
|
||||
[PR #404]: https://github.com/chenxiaolong/avbroot/pull/404
|
||||
[PR #408]: https://github.com/chenxiaolong/avbroot/pull/408
|
||||
[PR #409]: https://github.com/chenxiaolong/avbroot/pull/409
|
||||
[PR #410]: https://github.com/chenxiaolong/avbroot/pull/410
|
||||
[PR #411]: https://github.com/chenxiaolong/avbroot/pull/411
|
||||
[PR #415]: https://github.com/chenxiaolong/avbroot/pull/415
|
||||
[PR #418]: https://github.com/chenxiaolong/avbroot/pull/418
|
||||
[PR #421]: https://github.com/chenxiaolong/avbroot/pull/421
|
||||
[PR #422]: https://github.com/chenxiaolong/avbroot/pull/422
|
||||
[PR #423]: https://github.com/chenxiaolong/avbroot/pull/423
|
||||
[PR #424]: https://github.com/chenxiaolong/avbroot/pull/424
|
||||
[PR #425]: https://github.com/chenxiaolong/avbroot/pull/425
|
||||
[PR #427]: https://github.com/chenxiaolong/avbroot/pull/427
|
||||
[PR #428]: https://github.com/chenxiaolong/avbroot/pull/428
|
||||
[PR #429]: https://github.com/chenxiaolong/avbroot/pull/429
|
||||
[PR #430]: https://github.com/chenxiaolong/avbroot/pull/430
|
||||
[PR #434]: https://github.com/chenxiaolong/avbroot/pull/434
|
||||
[PR #435]: https://github.com/chenxiaolong/avbroot/pull/435
|
||||
[PR #437]: https://github.com/chenxiaolong/avbroot/pull/437
|
||||
[PR #438]: https://github.com/chenxiaolong/avbroot/pull/438
|
||||
[PR #439]: https://github.com/chenxiaolong/avbroot/pull/439
|
||||
|
||||
Generated
+286
-224
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.11.0"
|
||||
version = "3.15.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -121,7 +121,7 @@ When patching OTAs for multiple devices, generating unique keys for each device
|
||||
2. Convert the public key portion of the AVB signing key to the AVB public key metadata format. This is the format that the bootloader requires when setting the custom root of trust.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
|
||||
@@ -297,8 +297,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
--partition <name> # init_boot or boot, depending on device
|
||||
```
|
||||
|
||||
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
|
||||
@@ -385,11 +384,28 @@ Note that avbroot will validate that the prepatched image is compatible with the
|
||||
|
||||
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
|
||||
|
||||
### Skipping recovery OTA certificate patches
|
||||
### Skipping OTA certificate patches
|
||||
|
||||
avbroot can skip modifying `otacerts.zip` in the recovery image with the `--skip-recovery-ota-cert` option. **Do not do this unless you have a good reason to do so.** (For example, if you've already manually inserted the OTA certificate into a boot image specified with `--prepatched` or `--replace`.) When this option is used with `--rootless` (and `--dsu` is not specified), then no modifications are performed on any boot image besides ensuring they are properly signed.
|
||||
avbroot can skip modifying `otacerts.zip` with the `--skip-system-ota-cert` and `--skip-recovery-ota-cert` options. **Do not use these unless you have a good reason to do so.**
|
||||
|
||||
When manually adding the OTA certificate to a boot image, [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done.
|
||||
When `--skip-system-ota-cert` is used, the OTA certificates in the `system` partition will not be modified. This prevents custom OTA updater apps from installing further patched OTAs while booted into Android.
|
||||
|
||||
When `--skip-recovery-ota-cert` is used, the OTA certificates in the `vendor_boot` or `recovery` partition will not be modified. **This prevents sideloading further patched OTAs from recovery mode.**
|
||||
|
||||
If `--skip-recovery-ota-cert` is used because the OTA certificate was already manually added to the boot image, then [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done. The verification process is only capable of checking the boot image's copy of the OTA certificates, not the system image's copy of them.
|
||||
|
||||
### Skipping all patches
|
||||
|
||||
To have avbroot make the absolute minimal changes:
|
||||
|
||||
* Specify `--skip-system-ota-cert`
|
||||
* Specify `--skip-recovery-ota-cert`
|
||||
* Specify `--rootless`
|
||||
* Omit `--dsu`
|
||||
|
||||
This will re-sign the `vbmeta` partition and the OTA with the custom keys, but leave all other partitions untouched.
|
||||
|
||||
**This should only be used for advanced troubleshooting.** Without the OTA certificate patches, the resulting OTA will not be able to install further updates.
|
||||
|
||||
### Replacing partitions
|
||||
|
||||
@@ -415,6 +431,18 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
|
||||
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
|
||||
|
||||
### Changing virtual A/B CoW compression algorithm
|
||||
|
||||
The virtual A/B CoW compression algorithm can be changed by passing in `--vabc-algo <algo>` with `gz` or `lz4`. OTAs normally use an algorithm that is compatible with the initial version of Android shipped on the device.
|
||||
|
||||
* Devices launching with Android 12 support `gz` and `brotli` (unsupported by avbroot)
|
||||
* Devices launching with Android 14 support `lz4`
|
||||
* Devices launching with Android 15 support `zstd` (unsupported by avbroot)
|
||||
|
||||
Picking a fast algorithm, like lz4, can speed up OTA installation significantly when installing via a custom OTA updater app. However, there is no performance difference when sideloading an OTA from recovery mode.
|
||||
|
||||
Note that the currently running version of Android must support the specified compression algorithm or else the OTA will fail to install. For example, trying to install an Android 14 OTA that uses lz4 CoW compression will fail if the running system is Android 13.
|
||||
|
||||
### Non-interactive use
|
||||
|
||||
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
|
||||
@@ -451,17 +479,20 @@ avbroot prompts for the private key passphrases interactively by default. To run
|
||||
|
||||
* Use unencrypted private keys. This is strongly discouraged.
|
||||
|
||||
### Extracting the entire OTA
|
||||
### Extracting an OTA
|
||||
|
||||
To extract all images contained within the OTA's `payload.bin`, run:
|
||||
To extract the partition images contained within an OTA's `payload.bin`, run:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
By default, this only extracts the images that could potentially be patched by avbroot. To extract all images, use the `--all` option. To extract specific images, use the `--partition <name>` option, which can be specified multiple times.
|
||||
|
||||
This command also supports extracting the embedded OTA certificate and AVB public key using the `--cert-ota` and `--public-key-avb` options. To extract only these components, pass in `--none` to skip extracting partition images.
|
||||
|
||||
### Zip write mode
|
||||
|
||||
By default, avbroot uses streaming writes for the output OTA during patching. This means it computes the sha256 digest for the digital signature as the file is being written. This mode causes the zip file to contain data descriptors, which is part of the zip standard and works on the vast majority of devices. However, some devices may have broken zip file parsers and fail to properly read OTA zip files containing data descriptors. If this is the case, pass in `--zip-mode seekable` when patching.
|
||||
|
||||
+66
-10
@@ -43,7 +43,7 @@ avbroot модифицирует следующие образы:
|
||||
|
||||
## Использование
|
||||
|
||||
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
|
||||
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
|
||||
|
||||
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
|
||||
|
||||
@@ -107,7 +107,7 @@ avbroot модифицирует следующие образы:
|
||||
|
||||
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
|
||||
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
|
||||
1. Сгенерируйте ключи подписи для AVB и OTA.
|
||||
|
||||
@@ -119,7 +119,7 @@ avbroot модифицирует следующие образы:
|
||||
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
|
||||
@@ -295,6 +295,7 @@ Magisk версии 25211 и новее требует наличие разде
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
--partition <название раздела> # init_boot или boot, в зависимости от устройства
|
||||
```
|
||||
|
||||
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
|
||||
@@ -381,11 +382,16 @@ avbroot может подменить используемый загрузоч
|
||||
|
||||
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
|
||||
|
||||
### Пропуск патчинга сертификата OTA в разделе Recovery
|
||||
### Пропуск патчинга сертификата OTA
|
||||
|
||||
avbroot может пропустить изменение файла `otacerts.zip` в разделе Recovery с помощью опции `--skip-recovery-ota-cert`. **Не используйте эту функцию, если на то нет веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный образ (`boot.img`) и передаете его программе через опции `--prepatched` или `--replace`.) Если эта опция применяется совместно с `--rootless` (и без указания параметра `--dsu`), то в загрузочный образ не будут внесены никакие изменения, кроме обеспечения его корректной подписи.
|
||||
avbroot может пропускать изменение `otacerts.zip` с помощью аргументов `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный (boot) образ и передаете его программе через опции `--prepatched` или `--replace`.)
|
||||
В противном случае, на устройстве может не остаться возможности устанавливать дальнейшие обновления.
|
||||
|
||||
Если вы вручную добавили сертификат OTA в загрузочный образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
|
||||
При использовании `--skip-system-ota-cert`, никаких изменений в образ `system` не вносится.
|
||||
|
||||
При использовании `--skip-recovery-ota-cert` совместно с `--rootless` и без указания `--dsu`, не вносится никаких изменений в загрузочные образы, кроме обеспечения их корректной подписи.
|
||||
|
||||
Если вы вручную добавили сертификат OTA в загрузочный (boot) образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
|
||||
|
||||
### Подмена разделов
|
||||
|
||||
@@ -441,17 +447,20 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
|
||||
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
|
||||
|
||||
### Извлечение всей OTA
|
||||
### Извлечение образов из OTA
|
||||
|
||||
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
|
||||
Чтобы извлечь образы разделов, содержащихся в `payload.bin`, используйте команду:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
По умолчанию извлекаются только те образы, которые потенциально могут быть пропатчены с помощью avbroot. Чтобы извлечь все образы, используйте опцию `--all`. Для извлечения конкретных образов используйте опцию `--partition <название раздела>`, которую можно указать несколько раз.
|
||||
|
||||
Эта команда также поддерживает извлечение встроенного сертификата OTA и публичного ключа AVB с помощью опций `--cert-ota` и `--public-key-avb`. Чтобы извлечь только эти компоненты, укажите аргумент `--none`, чтобы пропустить извлечение образов разделов.
|
||||
|
||||
### Режим записи ZIP
|
||||
|
||||
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
|
||||
@@ -480,6 +489,53 @@ avbroot поддерживает делегирование всех опера
|
||||
|
||||
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
|
||||
|
||||
### Размер страницы 16 КБ в настройках для разработчиков
|
||||
|
||||
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
|
||||
|
||||
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
|
||||
|
||||
Поддержка `boot_otas` не планируется. Это потребует реализации функционала для модификации ФС в инкрементальных OTA и их дальнейшей обработки, что сделать очень непросто.
|
||||
|
||||
Если вы всё же хотите завести эту функцию, можно попробовать вручную подписать файлы в `boot_otas` собственным ключом. Поскольку инкрементальные OTA не пересоздаются, раздел `boot` должен оставаться без изменений во время выполнения команды `avbroot ota patch`.
|
||||
|
||||
1. Распакуйте `vendor.img` с помощью avbroot и [afsr](https://github.com/chenxiaolong/afsr):
|
||||
```bash
|
||||
avbroot avb unpack -i vendor.img
|
||||
afsr unpack -i raw.img
|
||||
```
|
||||
|
||||
2. Извлеките `payload.bin` из `boot_otas/boot_ota_16k.zip`.
|
||||
|
||||
3. Переподпишите `payload.bin` вашим OTA-ключом:
|
||||
```bash
|
||||
avbroot payload repack \
|
||||
-i payload.bin.orig \
|
||||
-o payload.bin \
|
||||
-k ota.key \
|
||||
--output-properties payload_properties.txt
|
||||
```
|
||||
|
||||
4. Создайте новый zip, включающий `payload.bin` и `payload_properties.txt`. Файлы должны быть добавлены без сжатия (например, с помощью `zip -0`).
|
||||
|
||||
5. Повторите эту процедуру для `boot_otas/boot_ota_4k.zip`.
|
||||
|
||||
6. Соберите `vendor.img` обратно и подпишите его вашим AVB-ключом:
|
||||
```bash
|
||||
afsr pack -o raw.img
|
||||
avbroot avb pack -o vendor.img -k avb.key --recompute-size
|
||||
```
|
||||
|
||||
7. Пропатчите обычный OTA-архив с прошивкой, подменив `vendor` на модифицированный образ:
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--replace vendor <модифицированный vendor.img> \
|
||||
<дальше указываются аргументы, как при обычном патчинге>
|
||||
```
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
|
||||
|
||||
+8
-12
@@ -13,28 +13,31 @@ anyhow = "1.0.75"
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
bzip2 = { version = "0.5.1", default-features = false, features = ["libbz2-rs-sys"] }
|
||||
cap-std = "3.0.0"
|
||||
cap-tempfile = "3.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
clap_complete = "4.4.0"
|
||||
cms = { version = "0.2.2", features = ["std"] }
|
||||
# We can't upgrade to 0.10.0 until x509-cert updates it too, since it's part of
|
||||
# the public API.
|
||||
const-oid = "0.9.5"
|
||||
crc32fast = "1.4.2"
|
||||
ctrlc = "3.4.0"
|
||||
dlv-list = "0.6.0"
|
||||
flate2 = "1.0.27"
|
||||
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.3.0"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
miniz_oxide = "0.8.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
passterm = "2.0.3"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
prost = "0.13.1"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
@@ -42,7 +45,7 @@ regex = { version = "1.9.4", default-features = false, features = ["perf", "std"
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
ring = "0.17.0"
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
sha1 = "0.10.5"
|
||||
@@ -57,13 +60,6 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
zerocopy = { version = "0.8.10", features = ["std"] }
|
||||
zerocopy-derive = "0.8.5"
|
||||
|
||||
# Waiting for next stable release.
|
||||
[dependencies.bzip2]
|
||||
git = "https://github.com/trifectatechfoundation/bzip2-rs"
|
||||
rev = "15258feb0cdc1114d6fc6b936254c4f4e1d5730c"
|
||||
default-features = false
|
||||
features = ["libbz2-rs-sys"]
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
@@ -73,10 +69,10 @@ features = ["deflate"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.158"
|
||||
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
|
||||
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
constcat = "0.5.0"
|
||||
constcat = "0.6.0"
|
||||
prost-build = "0.13.1"
|
||||
protox = "0.7.0"
|
||||
|
||||
|
||||
@@ -30,10 +30,13 @@ pub enum Command {
|
||||
Payload(payload::PayloadCli),
|
||||
Sparse(sparse::SparseCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
#[command(hide = true)]
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
#[command(hide = true)]
|
||||
Extract(ota::ExtractCli),
|
||||
/// (Deprecated: Use `avbroot boot magisk-info` instead.)
|
||||
#[command(hide = true)]
|
||||
MagiskInfo(boot::MagiskInfoCli),
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
crypto::write_pem_cert_file(&c.output, &cert)
|
||||
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::ExtractAvb(c) => {
|
||||
KeyCommand::ExtractAvb(c) | KeyCommand::EncodeAvb(c) => {
|
||||
let public_key = if let Some(p) = &c.input.key {
|
||||
let passphrase = get_passphrase_source(&c.passphrase, p);
|
||||
let private_key = crypto::read_pem_key_file(p, &passphrase)
|
||||
@@ -150,12 +150,12 @@ struct GenerateCertCli {
|
||||
validity: u64,
|
||||
}
|
||||
|
||||
/// Extract the AVB public key from a private key or certificate.
|
||||
/// Convert a key or certificate to an AVB-encoded public key.
|
||||
///
|
||||
/// The public key is stored in both the private key and the certificate. Either
|
||||
/// one can be used interchangeably.
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExtractAvbCli {
|
||||
struct EncodeAvbCli {
|
||||
/// Path to output AVB public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
@@ -183,7 +183,10 @@ struct DecodeAvbCli {
|
||||
enum KeyCommand {
|
||||
GenerateKey(GenerateKeyCli),
|
||||
GenerateCert(GenerateCertCli),
|
||||
ExtractAvb(ExtractAvbCli),
|
||||
/// (Deprecated: Use `avbroot key encode-avb` instead.)
|
||||
#[command(hide = true)]
|
||||
ExtractAvb(EncodeAvbCli),
|
||||
EncodeAvb(EncodeAvbCli),
|
||||
DecodeAvb(DecodeAvbCli),
|
||||
}
|
||||
|
||||
|
||||
+509
-181
File diff suppressed because it is too large
Load Diff
@@ -122,13 +122,13 @@ fn find_allocated_regions(
|
||||
loop {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
start = match rustix::fs::seek(reader, SeekFrom::Data(end as i64)) {
|
||||
start = match rustix::fs::seek(reader, SeekFrom::Data(end)) {
|
||||
Ok(offset) => offset,
|
||||
Err(e) if e == Errno::NXIO => break,
|
||||
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
|
||||
};
|
||||
|
||||
end = rustix::fs::seek(reader, SeekFrom::Hole(start as i64))
|
||||
end = rustix::fs::seek(reader, SeekFrom::Hole(start))
|
||||
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
|
||||
|
||||
result.push(start..end);
|
||||
|
||||
+11
-15
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -686,21 +686,17 @@ pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
|
||||
Ok(sd)
|
||||
}
|
||||
|
||||
/// Get a list of all standard X509 certificates contained within a
|
||||
/// Get an iterator to all standard X509 certificates contained within a
|
||||
/// [`SignedData`] structure.
|
||||
pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
|
||||
sd.certificates.as_ref().map_or_else(Vec::new, |certs| {
|
||||
certs
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
pub fn iter_cms_certs(sd: &SignedData) -> impl Iterator<Item = &Certificate> {
|
||||
sd.certificates.iter().flat_map(|certs| {
|
||||
certs.0.iter().filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+13
-14
@@ -163,7 +163,7 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub(crate) fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
|
||||
pub(crate) fn digest_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),
|
||||
@@ -292,7 +292,7 @@ trait DescriptorTag {
|
||||
|
||||
/// Raw on-disk layout for the AVB property descriptor after the prefix.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawPropertyDescriptor {
|
||||
key_size: big_endian::U64,
|
||||
value_size: big_endian::U64,
|
||||
@@ -395,7 +395,7 @@ impl<W: Write> ToWriter<W> for PropertyDescriptor {
|
||||
|
||||
/// Raw on-disk layout for the AVB hash tree descriptor after the prefix.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHashTreeDescriptor {
|
||||
dm_verity_version: big_endian::U32,
|
||||
image_size: big_endian::U64,
|
||||
@@ -534,7 +534,7 @@ impl HashTreeDescriptor {
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, false)?;
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm, false)?;
|
||||
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
|
||||
let (root_digest, hash_tree_data) = match ranges {
|
||||
Some(r) => {
|
||||
@@ -642,7 +642,7 @@ impl HashTreeDescriptor {
|
||||
) -> Result<()> {
|
||||
self.check_offsets()?;
|
||||
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, true)?;
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm, true)?;
|
||||
|
||||
util::check_bounds(self.tree_size, ..=HASH_TREE_MAX_SIZE)
|
||||
.map_err(|e| Error::IntOutOfBounds("HashTree::tree_size", e))?;
|
||||
@@ -860,7 +860,7 @@ impl<W: Write> ToWriter<W> for HashTreeDescriptor {
|
||||
|
||||
/// Raw on-disk layout for the AVB hash descriptor after the prefix.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHashDescriptor {
|
||||
image_size: big_endian::U64,
|
||||
hash_algorithm: [u8; 32],
|
||||
@@ -906,7 +906,7 @@ impl HashDescriptor {
|
||||
for_verify: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<ring::digest::Digest> {
|
||||
let algorithm = ring_algorithm(&self.hash_algorithm, for_verify)?;
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm, for_verify)?;
|
||||
let mut context = Context::new(algorithm);
|
||||
context.update(&self.salt);
|
||||
|
||||
@@ -1065,7 +1065,7 @@ impl<W: Write> ToWriter<W> for HashDescriptor {
|
||||
/// Raw on-disk layout for the AVB kernel command line descriptor after the
|
||||
/// prefix.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawKernelCmdlineDescriptor {
|
||||
flags: big_endian::U32,
|
||||
cmdline_len: big_endian::U32,
|
||||
@@ -1139,7 +1139,7 @@ impl<W: Write> ToWriter<W> for KernelCmdlineDescriptor {
|
||||
|
||||
/// Raw on-disk layout for the AVB chain partition descriptor after the prefix.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawChainPartitionDescriptor {
|
||||
rollback_index_location: big_endian::U32,
|
||||
partition_name_len: big_endian::U32,
|
||||
@@ -1258,7 +1258,7 @@ impl<W: Write> ToWriter<W> for ChainPartitionDescriptor {
|
||||
|
||||
/// Raw on-disk layout for the AVB descriptor prefix.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawDescriptor {
|
||||
tag: big_endian::U64,
|
||||
num_bytes_following: big_endian::U64,
|
||||
@@ -1458,7 +1458,7 @@ impl<'a> TryFrom<&'a mut Descriptor> for AppendedDescriptorMut<'a> {
|
||||
|
||||
/// Raw on-disk layout for the AVB header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HEADER_MAGIC`].
|
||||
magic: [u8; 4],
|
||||
@@ -1927,7 +1927,7 @@ impl<W: Write> ToWriter<W> for Header {
|
||||
|
||||
/// Raw on-disk layout for the AVB footer.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawFooter {
|
||||
/// Magic value. This should be equal to [`FOOTER_MAGIC`].
|
||||
magic: [u8; 4],
|
||||
@@ -2015,10 +2015,9 @@ impl<W: Write> ToWriter<W> for Footer {
|
||||
|
||||
/// Raw on-disk layout for the AVB binary public key header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawPublicKey {
|
||||
key_num_bits: big_endian::U32,
|
||||
#[expect(unused)]
|
||||
n0inv: big_endian::U32,
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ pub trait BootImageExt {
|
||||
|
||||
/// Raw on-disk layout for the v0 image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawV0 {
|
||||
/// Magic value. This should be equal to [`BOOT_MAGIC`].
|
||||
magic: [u8; 8],
|
||||
@@ -139,7 +139,7 @@ struct RawV0 {
|
||||
|
||||
/// Raw on-disk layout for the extra v1 image header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawV1Extra {
|
||||
recovery_dtbo_size: little_endian::U32,
|
||||
recovery_dtbo_offset: little_endian::U64,
|
||||
@@ -148,7 +148,7 @@ struct RawV1Extra {
|
||||
|
||||
/// Raw on-disk layout for the extra v2 image header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawV2Extra {
|
||||
dtb_size: little_endian::U32,
|
||||
dtb_addr: little_endian::U64,
|
||||
@@ -595,7 +595,7 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
|
||||
|
||||
/// Raw on-disk layout for the v3 image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawV3 {
|
||||
/// Magic value. This should be equal to [`BOOT_MAGIC`].
|
||||
magic: [u8; 8],
|
||||
@@ -610,7 +610,7 @@ struct RawV3 {
|
||||
|
||||
/// Raw on-disk layout for the extra v4 image header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawV4Extra {
|
||||
signature_size: little_endian::U32,
|
||||
}
|
||||
@@ -961,7 +961,7 @@ impl<W: Write> ToWriter<W> for BootImageV3Through4 {
|
||||
|
||||
/// Raw on-disk layout for the vendor v3 image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawVendorV3 {
|
||||
/// Magic value. This should be equal to [`VENDOR_BOOT_MAGIC`].
|
||||
magic: [u8; 8],
|
||||
@@ -980,7 +980,7 @@ struct RawVendorV3 {
|
||||
|
||||
/// Raw on-disk layout for the extra vendor v4 image header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawVendorV4Extra {
|
||||
vendor_ramdisk_table_size: little_endian::U32,
|
||||
vendor_ramdisk_table_entry_num: little_endian::U32,
|
||||
@@ -990,7 +990,7 @@ struct RawVendorV4Extra {
|
||||
|
||||
/// Raw on-disk layout for the vendor v4 ramdisk table entry.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawVendorV4RamdiskTableEntry {
|
||||
ramdisk_size: little_endian::U32,
|
||||
ramdisk_offset: little_endian::U32,
|
||||
|
||||
@@ -76,7 +76,7 @@ pub struct InvalidHexCharError(RawHexU32, char);
|
||||
|
||||
/// ASCII-encoded hex integer value used in cpio header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHexU32([u8; 8]);
|
||||
|
||||
impl fmt::Debug for RawHexU32 {
|
||||
@@ -121,7 +121,7 @@ impl TryFrom<RawHexU32> for u32 {
|
||||
|
||||
/// Raw on-disk layout for the cpio header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
|
||||
magic: [u8; 6],
|
||||
|
||||
@@ -592,7 +592,7 @@ impl Fec {
|
||||
|
||||
/// Raw on-disk layout for the FEC image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`FEC_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
|
||||
@@ -431,7 +431,7 @@ impl HashTree {
|
||||
|
||||
/// Raw on-disk layout for our custom hash tree image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
|
||||
magic: [u8; 16],
|
||||
@@ -492,8 +492,8 @@ impl HashTreeImage {
|
||||
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::ring_algorithm(name, false)
|
||||
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::digest_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
|
||||
}
|
||||
|
||||
@@ -509,8 +509,8 @@ impl HashTreeImage {
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let ring_algorithm = Self::ring_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
|
||||
let digest_algorithm = Self::digest_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -530,8 +530,8 @@ impl HashTreeImage {
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
self.root_digest = hash_tree.update(
|
||||
input,
|
||||
@@ -550,8 +550,8 @@ impl HashTreeImage {
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
hash_tree.verify(
|
||||
input,
|
||||
|
||||
+55
-57
@@ -1,8 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt,
|
||||
io::{self, Read, Seek, Write},
|
||||
mem,
|
||||
@@ -152,10 +151,8 @@ pub enum Error {
|
||||
ExtentTypeZeroNotEmpty { index: usize },
|
||||
#[error("Extent #{index}: Invalid type: {extent_type}")]
|
||||
ExtentInvalidType { index: usize, extent_type: u32 },
|
||||
#[error("Extent #{index}: Overlaps previous extent")]
|
||||
ExtentOverlapsPrevious { index: usize },
|
||||
#[error("Extent #{index}: Earlier block device index than previous extent")]
|
||||
ExtentDeviceNotConsecutive { index: usize },
|
||||
#[error("Extent #{index}: Overlaps another extent: #{other}")]
|
||||
ExtentOverlapsAnother { index: usize, other: usize },
|
||||
#[error("Extent #{index}: Block device index too large")]
|
||||
ExtentDeviceIndexTooLarge { index: usize },
|
||||
// Partition group errors.
|
||||
@@ -253,7 +250,7 @@ impl PartitionAttributes {
|
||||
|
||||
/// Raw on-disk layout for the metadata geometry.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawGeometry {
|
||||
/// Magic value. This should be equal to [`GEOMETRY_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
@@ -335,7 +332,7 @@ impl RawGeometry {
|
||||
|
||||
/// Raw on-disk layout for a table descriptor within a [`RawHeader`].
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawTableDescriptor {
|
||||
/// Offset relative to the end of the [`RawHeader`].
|
||||
offset: little_endian::U32,
|
||||
@@ -394,7 +391,7 @@ impl RawTableDescriptor {
|
||||
|
||||
/// Raw on-disk layout for the metadata header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HEADER_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
@@ -572,7 +569,7 @@ impl RawHeader {
|
||||
|
||||
/// A potentially invalid raw partition name string.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct PartitionName([u8; 36]);
|
||||
|
||||
impl fmt::Debug for PartitionName {
|
||||
@@ -598,17 +595,15 @@ impl PartitionName {
|
||||
|
||||
fn validate(&self) -> Result<()> {
|
||||
let (prefix, suffix) = self.split();
|
||||
let mut has_alnum = false;
|
||||
|
||||
for b in prefix {
|
||||
match b {
|
||||
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' => has_alnum = true,
|
||||
b'_' => {}
|
||||
_ => return Err(Error::PartitionNameInvalid(DebugString::new(self))),
|
||||
}
|
||||
}
|
||||
// AOSP liblp's metadata_format.h says "Characters may only be
|
||||
// alphanumeric or _", but AOSP creates partitions named like
|
||||
// "system_b-cow".
|
||||
let prefix_valid = prefix
|
||||
.iter()
|
||||
.all(|b| matches!(*b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-'));
|
||||
|
||||
if has_alnum && is_zero(suffix) {
|
||||
if prefix_valid && is_zero(suffix) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::PartitionNameInvalid(DebugString::new(self)))
|
||||
@@ -644,7 +639,7 @@ impl FromStr for PartitionName {
|
||||
|
||||
/// Raw on-disk layout for an entry in the logical partitions table.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawPartition {
|
||||
/// Partition name in ASCII. This must be unique across all partitions.
|
||||
name: PartitionName,
|
||||
@@ -706,7 +701,7 @@ impl RawPartition {
|
||||
.first_extent_index
|
||||
.get()
|
||||
.checked_add(self.num_extents.get())
|
||||
.map_or(true, |n| n as usize > extents.len())
|
||||
.is_none_or(|n| n as usize > extents.len())
|
||||
{
|
||||
return Err(Error::PartitionExtentIndicesTooLarge {
|
||||
name: DebugString::new(self.name),
|
||||
@@ -735,7 +730,7 @@ impl RawPartition {
|
||||
|
||||
/// Raw on-disk layout for an entry in the extent table.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawExtent {
|
||||
/// Number of [`SECTOR_SIZE`]-byte sectors in this extent.
|
||||
num_sectors: little_endian::U64,
|
||||
@@ -827,7 +822,7 @@ impl RawExtent {
|
||||
|
||||
/// Raw on-disk layout for an entry in the partition groups table.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawPartitionGroup {
|
||||
/// Partition group name in ASCII. This must be unique across all groups.
|
||||
name: PartitionName,
|
||||
@@ -894,7 +889,7 @@ impl RawPartitionGroup {
|
||||
|
||||
/// Raw on-disk layout for an entry in the block devices table.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawBlockDevice {
|
||||
/// The first [`SECTOR_SIZE`]-byte sector where actual data for the logical
|
||||
/// partitions can be allocated.
|
||||
@@ -1010,23 +1005,29 @@ impl RawMetadataSlot {
|
||||
extent.validate(i, &self.block_devices)?;
|
||||
}
|
||||
|
||||
// Ensure that all extents are in increasing order and not overlapping.
|
||||
let mut iter = self
|
||||
// Ensure that all extents are not overlapping. We have to sort here
|
||||
// because the extents may not be in order when loading the super
|
||||
// partition on an actual device. Also, AOSP liblp's `metadata_format.h`
|
||||
// says "Gaps between extents are not allowed", but AOSP frequently
|
||||
// creates this situation after a virtual A/B CoW merge.
|
||||
let mut sorted_extents = self
|
||||
.extents
|
||||
.iter()
|
||||
.filter(|e| e.target_type.get() == RawExtent::TARGET_TYPE_LINEAR)
|
||||
.enumerate();
|
||||
while let (Some((_, a)), Some((i, b))) = (iter.next(), iter.next()) {
|
||||
match a.target_source.get().cmp(&b.target_source.get()) {
|
||||
Ordering::Equal => {
|
||||
if a.target_data.get() + a.num_sectors.get() > b.target_data.get() {
|
||||
return Err(Error::ExtentOverlapsPrevious { index: i });
|
||||
}
|
||||
}
|
||||
Ordering::Greater => {
|
||||
return Err(Error::ExtentDeviceNotConsecutive { index: i });
|
||||
}
|
||||
Ordering::Less => {}
|
||||
.enumerate()
|
||||
.filter(|(_, e)| e.target_type.get() == RawExtent::TARGET_TYPE_LINEAR)
|
||||
.collect::<Vec<_>>();
|
||||
sorted_extents.sort_by_key(|(_, e)| (e.target_source, e.target_data));
|
||||
|
||||
for window in sorted_extents.windows(2) {
|
||||
let ((a_i, a), (b_i, b)) = (window[0], window[1]);
|
||||
|
||||
if a.target_source == b.target_source
|
||||
&& a.target_data.get() + a.num_sectors.get() > b.target_data.get()
|
||||
{
|
||||
return Err(Error::ExtentOverlapsAnother {
|
||||
index: b_i,
|
||||
other: a_i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,14 +1061,19 @@ impl RawMetadata {
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| Error::DataRead("geometry", e))?;
|
||||
|
||||
let image_type = if util::is_zero(&buf) {
|
||||
ImageType::Normal
|
||||
} else {
|
||||
ImageType::Empty
|
||||
};
|
||||
// For non-empty images, AOSP says the first block is supposed to be
|
||||
// filled with zeros, but Samsung puts their own SignerVer02 structure
|
||||
// in there, so we can't rely on that.
|
||||
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
|
||||
let geometry = match image_type {
|
||||
ImageType::Normal => {
|
||||
let image_type = match geometry.validate() {
|
||||
Ok(()) => {
|
||||
// This is an empty image for use with fastboot. These have no
|
||||
// extra padding at the beginning of the file nor backup copies
|
||||
// of the geometry and metadata structs.
|
||||
ImageType::Empty
|
||||
}
|
||||
Err(Error::GeometryInvalidMagic(_)) => {
|
||||
// This is an normal non-empty image, which has extra padding at
|
||||
// the beginning to avoid having the geometry struct interpreted
|
||||
// as a boot sector.
|
||||
@@ -1077,7 +1083,7 @@ impl RawMetadata {
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| Error::DataRead("geometry_primary", e))?;
|
||||
|
||||
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
|
||||
if geometry.validate().is_ok() {
|
||||
// Skip the backup copy.
|
||||
@@ -1094,17 +1100,9 @@ impl RawMetadata {
|
||||
geometry.validate()?;
|
||||
}
|
||||
|
||||
geometry
|
||||
}
|
||||
ImageType::Empty => {
|
||||
// This is an empty image for use with fastboot. These have no
|
||||
// extra padding at the beginning of the file nor backup copies
|
||||
// of the geometry and metadata structs.
|
||||
let geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
geometry.validate()?;
|
||||
|
||||
geometry
|
||||
ImageType::Normal
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
Ok((image_type, geometry.to_owned()))
|
||||
|
||||
+136
-76
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -15,7 +15,7 @@ use cms::signed_data::SignedData;
|
||||
use const_oid::{db::rfc5912, ObjectIdentifier};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use ring::digest::Context;
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{der::Encode, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
@@ -565,11 +565,131 @@ pub fn verify_metadata(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the CMS signature from the OTA zip comment. Returns the decoded CMS
|
||||
/// [`SignedData`] structure and the length of the file (from the beginning)
|
||||
/// that's covered by the signature. This does not perform any parsing of zip
|
||||
/// data structures.
|
||||
fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
#[derive(Clone, Debug)]
|
||||
struct RawOtaSignature {
|
||||
/// Decoded CMS structure.
|
||||
signed_data: SignedData,
|
||||
/// Length of the file (from the beginning) that's covered by the signature.
|
||||
hashed_size: u64,
|
||||
}
|
||||
|
||||
impl RawOtaSignature {
|
||||
pub fn embedded_cert(&self) -> Result<&Certificate> {
|
||||
let mut iter = crypto::iter_cms_certs(&self.signed_data);
|
||||
|
||||
let Some(cert) = iter.next() else {
|
||||
return Err(Error::NotOneCmsCertificate(0));
|
||||
};
|
||||
|
||||
let None = iter.next() else {
|
||||
return Err(Error::NotOneCmsCertificate(2 + iter.count()));
|
||||
};
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OtaSignature {
|
||||
pub cert: Certificate,
|
||||
pub digest_algo: &'static Algorithm,
|
||||
pub sig_algo: SignatureAlgorithm,
|
||||
pub sig: Vec<u8>,
|
||||
pub data_size: u64,
|
||||
}
|
||||
|
||||
impl TryFrom<RawOtaSignature> for OtaSignature {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_ota_sig: RawOtaSignature) -> Result<Self> {
|
||||
let cert = raw_ota_sig.embedded_cert()?;
|
||||
|
||||
// Make sure this is a signature scheme we can handle. There's currently
|
||||
// no Rust library to verify arbitrary CMS signatures for large files
|
||||
// without fully reading them into memory.
|
||||
let signers_len = raw_ota_sig.signed_data.signer_infos.0.len();
|
||||
if signers_len != 1 {
|
||||
return Err(Error::NotOneCmsSignerInfo(signers_len));
|
||||
}
|
||||
|
||||
let signer = raw_ota_sig.signed_data.signer_infos.0.get(0).unwrap();
|
||||
if signer.digest_alg.oid != rfc5912::ID_SHA_256
|
||||
&& signer.digest_alg.oid != rfc5912::ID_SHA_1
|
||||
{
|
||||
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
|
||||
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
|
||||
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
|
||||
{
|
||||
return Err(Error::UnsupportedSignatureAlgorithm(
|
||||
signer.signature_algorithm.oid,
|
||||
));
|
||||
}
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (digest_algo, sig_algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
|
||||
} else {
|
||||
(
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
SignatureAlgorithm::Sha1WithRsa,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
cert: cert.clone(),
|
||||
digest_algo,
|
||||
sig_algo,
|
||||
sig: signer.signature.as_bytes().to_vec(),
|
||||
data_size: raw_ota_sig.hashed_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OtaSignature {
|
||||
/// Verify an OTA zip against its embedded certificate. This function makes
|
||||
/// no assertion about whether the certificate is actually trusted.
|
||||
///
|
||||
/// CMS signed attributes are intentionally not supported because AOSP
|
||||
/// recovery does not support them either. It expects the CMS [`SignedData`]
|
||||
/// structure to be used for nothing more than a raw signature transport
|
||||
/// mechanism.
|
||||
pub fn verify_ota(
|
||||
&self,
|
||||
mut reader: impl Read + Seek,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let public_key = crypto::get_public_key(&self.cert).map_err(Error::OtaCertExtractPubKey)?;
|
||||
|
||||
// Manually hash the parts of the file covered by the signature.
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let mut hashing_reader = HashingReader::new(reader, Context::new(self.digest_algo));
|
||||
|
||||
stream::copy_n(
|
||||
&mut hashing_reader,
|
||||
io::sink(),
|
||||
self.data_size,
|
||||
cancel_signal,
|
||||
)
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let (_, context) = hashing_reader.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key
|
||||
.verify_sig(self.sig_algo, digest.as_ref(), &self.sig)
|
||||
.map_err(Error::CmsVerify)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the CMS signature from the OTA zip comment. This does not perform any
|
||||
/// parsing of zip data structures.
|
||||
fn parse_raw_ota_sig(mut reader: impl Read + Seek) -> Result<RawOtaSignature> {
|
||||
let file_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
@@ -614,82 +734,22 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
}
|
||||
|
||||
let sig_offset = eocd_size as usize - usize::from(abs_eoc_offset);
|
||||
let sd =
|
||||
let signed_data =
|
||||
crypto::parse_cms(&eocd[sig_offset..eocd_size as usize - 6]).map_err(Error::CmsLoad)?;
|
||||
// The signature covers everything aside from the archive comment and its
|
||||
// length field.
|
||||
let hashed_size = file_size - 2 - u64::from(comment_size);
|
||||
|
||||
Ok((sd, hashed_size))
|
||||
Ok(RawOtaSignature {
|
||||
signed_data,
|
||||
hashed_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify an OTA zip against its embedded certificates. This function makes no
|
||||
/// assertion about whether the certificate is actually trusted. Returns the
|
||||
/// embedded certificate.
|
||||
///
|
||||
/// CMS signed attributes are intentionally not supported because AOSP recovery
|
||||
/// does not support them either. It expects the CMS [`SignedData`] structure to
|
||||
/// be used for nothing more than a raw signature transport mechanism.
|
||||
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
|
||||
let (sd, hashed_size) = parse_ota_sig(&mut reader)?;
|
||||
|
||||
// Make sure the certificate in the CMS structure matches the otacert zip
|
||||
// entry.
|
||||
let certs = crypto::get_cms_certs(&sd);
|
||||
if certs.len() != 1 {
|
||||
return Err(Error::NotOneCmsCertificate(certs.len()));
|
||||
}
|
||||
|
||||
let cert = &certs[0];
|
||||
let public_key = crypto::get_public_key(cert).map_err(Error::OtaCertExtractPubKey)?;
|
||||
|
||||
// Make sure this is a signature scheme we can handle. There's currently no
|
||||
// Rust library to verify arbitrary CMS signatures for large files without
|
||||
// fully reading them into memory.
|
||||
if sd.signer_infos.0.len() != 1 {
|
||||
return Err(Error::NotOneCmsSignerInfo(sd.signer_infos.0.len()));
|
||||
}
|
||||
|
||||
let signer = sd.signer_infos.0.get(0).unwrap();
|
||||
if signer.digest_alg.oid != rfc5912::ID_SHA_256 && signer.digest_alg.oid != rfc5912::ID_SHA_1 {
|
||||
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
|
||||
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
|
||||
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
|
||||
{
|
||||
return Err(Error::UnsupportedSignatureAlgorithm(
|
||||
signer.signature_algorithm.oid,
|
||||
));
|
||||
}
|
||||
|
||||
// Manually hash the parts of the file covered by the signature.
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
|
||||
} else {
|
||||
(
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
SignatureAlgorithm::Sha1WithRsa,
|
||||
)
|
||||
};
|
||||
|
||||
let mut hashing_reader = HashingReader::new(reader, Context::new(algorithm));
|
||||
|
||||
stream::copy_n(&mut hashing_reader, io::sink(), hashed_size, cancel_signal)
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let (_, context) = hashing_reader.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key
|
||||
.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())
|
||||
.map_err(Error::CmsVerify)?;
|
||||
|
||||
Ok(cert.clone())
|
||||
/// Parse the signature information from the CMS signature embedded in the OTA
|
||||
/// zip archive comment.
|
||||
pub fn parse_ota_sig(reader: impl Read + Seek) -> Result<OtaSignature> {
|
||||
parse_raw_ota_sig(reader)?.try_into()
|
||||
}
|
||||
|
||||
/// Get and parse the protobuf-encoded OTA metadata, the PEM-encoded otacert,
|
||||
|
||||
+131
-55
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -12,6 +12,8 @@ use std::{
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use bzip2::write::BzDecoder;
|
||||
use clap::ValueEnum;
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
@@ -47,6 +49,9 @@ const PAYLOAD_VERSION: u64 = 2;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Size of each extent. This matches what AOSP's delta_generator does.
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
@@ -121,6 +126,8 @@ pub enum Error {
|
||||
InputOpen(String, #[source] io::Error),
|
||||
#[error("Failed to open output file for partition: {0}")]
|
||||
OutputOpen(String, #[source] io::Error),
|
||||
#[error("Failed to GZ compress partition image chunk")]
|
||||
GzCompress(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
#[error("Failed to XZ compress partition image chunk")]
|
||||
@@ -135,7 +142,7 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Raw on-disk layout for the payload header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`PAYLOAD_MAGIC`].
|
||||
magic: [u8; 4],
|
||||
@@ -942,52 +949,136 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
Ok((data, digest_compressed))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
|
||||
pub enum VabcAlgo {
|
||||
Lz4,
|
||||
Gzip,
|
||||
Gz,
|
||||
}
|
||||
|
||||
impl VabcAlgo {
|
||||
pub fn new(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"lz4" => Some(Self::Lz4),
|
||||
"gz" => Some(Self::Gzip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> u64 {
|
||||
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> Result<u64> {
|
||||
let mut total = 0;
|
||||
|
||||
while !raw_data.is_empty() {
|
||||
let n = raw_data.len().min(block_size as usize);
|
||||
let (chunk, remaining) = raw_data.split_at(n);
|
||||
|
||||
// This should match CompressWorker::GetDefaultCompressionLevel() in
|
||||
// AOSP's libsnapshot.
|
||||
let compressed = match self {
|
||||
Self::Lz4 => lz4_flex::block::compress(&raw_data[..n]),
|
||||
// We use the miniz_oxide backend for flate2, but flate2 doesn't
|
||||
// expose a nice function for compressing to a vec, so just use
|
||||
// miniz_oxide directly.
|
||||
Self::Gzip => miniz_oxide::deflate::compress_to_vec_zlib(&raw_data[..n], 9),
|
||||
Self::Lz4 => lz4_flex::block::compress(chunk),
|
||||
Self::Gz => {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(chunk).map_err(Error::GzCompress)?;
|
||||
encoder.finish().map_err(Error::GzCompress)?
|
||||
}
|
||||
};
|
||||
|
||||
total += compressed.len().min(n) as u64;
|
||||
|
||||
raw_data = &raw_data[n..];
|
||||
raw_data = remaining;
|
||||
}
|
||||
|
||||
total
|
||||
Ok(total)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VabcAlgo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Lz4 => f.write_str("lz4"),
|
||||
Self::Gzip => f.write_str("gz"),
|
||||
}
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add fudge factor to CoW estimate to account for overhead.
|
||||
fn fudge_cow_estimate(mut estimate: u64) -> Option<u64> {
|
||||
// lz4_flex and zlib-rs usually compress better than the lz4 and zlib
|
||||
// implementations used by libsnapshot_cow. Make up for this by adding
|
||||
// percentage-based overhead.
|
||||
estimate = estimate.checked_add(estimate / 100)?;
|
||||
|
||||
// We also need to account for constant overhead, especially with smaller
|
||||
// partitions. We can match what delta_generator normally adds in
|
||||
// CowWriterV2::InitPos() exactly. Since we only ever create full OTAs, we
|
||||
// can assume that all CoW operations are kCowReplaceOp.
|
||||
|
||||
// sizeof(CowHeader).
|
||||
estimate = estimate.checked_add(38)?;
|
||||
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
|
||||
estimate = estimate.checked_add(2 * 1024 * 1024)?;
|
||||
// CowOptions::cluster_ops * sizeof(CowOperationV2).
|
||||
estimate = estimate.checked_add(200 * 20)?;
|
||||
|
||||
Some(estimate)
|
||||
}
|
||||
|
||||
/// Compute the VABC CoW v2 size estimate. The caller must update
|
||||
/// [`PartitionUpdate::estimate_cow_size`] with this value or else update_engine
|
||||
/// may fail to flash the partition due to running out of space on the CoW block
|
||||
/// device. CoW v2 + other algorithms and also CoW v3 are currently unsupported
|
||||
/// because there currently are no known OTAs that use those configurations.
|
||||
pub fn compute_cow_estimate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
vabc_algo: VabcAlgo,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
let file_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut r| r.seek(SeekFrom::End(0)))
|
||||
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
return Err(Error::InvalidPartitionSize {
|
||||
name: partition_name.to_owned(),
|
||||
size: file_size,
|
||||
block_size,
|
||||
});
|
||||
}
|
||||
|
||||
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
|
||||
|
||||
let cow_estimate = (0..chunks_total)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<u64> {
|
||||
let data = (|| {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
|
||||
file_size % CHUNK_SIZE
|
||||
} else {
|
||||
CHUNK_SIZE
|
||||
};
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_vec_exact(chunk_size as usize)
|
||||
})()
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
vabc_algo.compressed_size(&data, block_size)
|
||||
})
|
||||
.try_fold(
|
||||
|| 0u64,
|
||||
|total, chunk_estimate| -> Result<u64> {
|
||||
total
|
||||
.checked_add(chunk_estimate?)
|
||||
.ok_or(Error::IntOverflow("cow_estimate"))
|
||||
},
|
||||
)
|
||||
.try_reduce(
|
||||
|| 0u64,
|
||||
|total, partial| {
|
||||
total
|
||||
.checked_add(partial)
|
||||
.ok_or(Error::IntOverflow("cow_estimate"))
|
||||
},
|
||||
)?;
|
||||
|
||||
fudge_cow_estimate(cow_estimate).ok_or(Error::IntOverflow("cow_estimate_fudged"))
|
||||
}
|
||||
|
||||
/// Compress the image and return the corresponding information to insert into
|
||||
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
|
||||
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
|
||||
@@ -996,12 +1087,9 @@ impl fmt::Display for VabcAlgo {
|
||||
/// update [`InstallOperation::data_offset`] in each operation manually because
|
||||
/// the initial values are relative to 0.
|
||||
///
|
||||
/// If `vabc_algo` is set, the VABC CoW v2 size estimate will be computed. The
|
||||
/// caller must update [`PartitionUpdate::estimate_cow_size`] with this value or
|
||||
/// else update_engine may fail to flash the partition due to running out of
|
||||
/// space on the CoW block device. CoW v2 + other algorithms and also CoW v3 are
|
||||
/// currently unsupported because there currently are no known OTAs that use
|
||||
/// those configurations.
|
||||
/// If `vabc_algo` is set, the VABC CoW size estimate will also be computed.
|
||||
/// This is more efficient than separately calling [`compute_cow_estimate`]
|
||||
/// since the input does not need to be read twice.
|
||||
pub fn compress_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
@@ -1010,7 +1098,6 @@ pub fn compress_image(
|
||||
vabc_algo: Option<VabcAlgo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
let file_size = input
|
||||
@@ -1028,9 +1115,9 @@ pub fn compress_image(
|
||||
}
|
||||
|
||||
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
|
||||
let mut bytes_compressed = 0;
|
||||
let mut bytes_compressed = 0u64;
|
||||
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
|
||||
let mut cow_estimate = 0;
|
||||
let mut cow_estimate = 0u64;
|
||||
let mut operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
@@ -1069,8 +1156,10 @@ pub fn compress_image(
|
||||
.map(
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
let cow_size =
|
||||
vabc_algo.map_or(0, |a| a.compressed_size(&raw_data, block_size));
|
||||
let cow_size = vabc_algo
|
||||
.map(|a| a.compressed_size(&raw_data, block_size))
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
@@ -1090,8 +1179,12 @@ pub fn compress_image(
|
||||
|
||||
for (data, operation, cow_size) in &mut compressed_data_group {
|
||||
operation.data_offset = Some(bytes_compressed);
|
||||
bytes_compressed += data.len() as u64;
|
||||
cow_estimate += *cow_size;
|
||||
bytes_compressed = bytes_compressed
|
||||
.checked_add(data.len() as u64)
|
||||
.ok_or(Error::IntOverflow("bytes_compressed"))?;
|
||||
cow_estimate = cow_estimate
|
||||
.checked_add(*cow_size)
|
||||
.ok_or(Error::IntOverflow("cow_estimate"))?;
|
||||
}
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
@@ -1116,24 +1209,7 @@ pub fn compress_image(
|
||||
};
|
||||
|
||||
let cow_estimate = if vabc_algo.is_some() {
|
||||
// lz4_flex and miniz_oxide usually compress better than the lz4 and
|
||||
// zlib implementations used by libsnapshot_cow. Make up for this by
|
||||
// adding percentage-based overhead.
|
||||
cow_estimate += cow_estimate / 100;
|
||||
|
||||
// We also need to account for constant overhead, especially with
|
||||
// smaller partitions. We can match what delta_generator normally adds
|
||||
// in CowWriterV2::InitPos() exactly. Since we only ever create full
|
||||
// OTAs, we can assume that all CoW operations are kCowReplaceOp.
|
||||
|
||||
// sizeof(CowHeader).
|
||||
cow_estimate += 38;
|
||||
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
|
||||
cow_estimate += 2 * 1024 * 1024;
|
||||
// CowOptions::cluster_ops * sizeof(CowOperationV2).
|
||||
cow_estimate += 200 * 20;
|
||||
|
||||
Some(cow_estimate)
|
||||
Some(fudge_cow_estimate(cow_estimate).ok_or(Error::IntOverflow("cow_estimate_fudged"))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -106,7 +106,7 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Raw on-disk layout for the header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HEADER_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
@@ -183,7 +183,7 @@ impl RawHeader {
|
||||
|
||||
/// Raw on-disk layout for the chunk header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
#[repr(C, packed)]
|
||||
struct RawChunk {
|
||||
/// Chunk type. Must be [`CHUNK_TYPE_RAW`], [`CHUNK_TYPE_FILL`],
|
||||
/// [`CHUNK_TYPE_DONT_CARE`], or [`CHUNK_TYPE_CRC32`].
|
||||
|
||||
@@ -37,8 +37,8 @@ allow = [
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
@@ -70,5 +70,4 @@ unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
"https://github.com/trifectatechfoundation/bzip2-rs",
|
||||
]
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.0"
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
|
||||
+11
-11
@@ -50,11 +50,11 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
|
||||
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
|
||||
patched = "6c27ffb07f4497af8539f8283e506066af9417580230c3209c9875fc15d5069d"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
|
||||
patched = "e7b4609ba7a23609211dcae143bc43f091f286fbbb3a9301c02ee25614d35deb"
|
||||
patched = "e4fc12523ffc312796b92210bc1e3bbb70dd60797a47daae76c8b5852e48b382"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -91,11 +91,11 @@ data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_streaming]
|
||||
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
|
||||
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
|
||||
patched = "6832ded3e98a14edc8c5ea7284fcea0b958fa710ebf222c27116faec8dfefe2e"
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_seekable]
|
||||
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
|
||||
patched = "3456b14e014cf565a808a9e834d9105a23539f07b2c460db19c9384aadbc3b93"
|
||||
patched = "114f8936e917d7e4a71bc1521adb3c8e676de3a738f8c7c505b86464d20bd95c"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
@@ -133,17 +133,17 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
|
||||
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
|
||||
patched = "1f28d9210a17e233cd5da4af55b07db764b19eeab991394514170b405240464f"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
|
||||
patched = "9f6342940b7cfbeb27b0567f006bb35cbee910ef038ec535403c662d5252ca71"
|
||||
patched = "27b80c7be9c1e527ea26abe3dabde245c580e6f26ec084204278fbfd81a39f83"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
|
||||
[profile.pixel_v2]
|
||||
vabc_algo = "Gzip"
|
||||
vabc_algo = "Gz"
|
||||
|
||||
[profile.pixel_v2.partitions.boot]
|
||||
avb.signed = false
|
||||
@@ -168,9 +168,9 @@ data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
|
||||
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
|
||||
original = "bd2f19cf3d2285e35e8b36d44f75ed910e8e0be44c3ebd29f17a812521ba754b"
|
||||
patched = "cf65d5b90500af54cd1204a646379bb852825061bc7c3f973b7a042f353f75ad"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "52284308fae10cbaf09ade14e92f3bbe6149751a42bff15432982fcef8d890ab"
|
||||
patched = "7ad74ac87ddcaf34938017e6149a646041d70926e31ecda93e156e9397467b3b"
|
||||
original = "7f96ebf7366e0b60c91ac1e5f196a2189ffdb0bbc73f77804a736466fcab7315"
|
||||
patched = "c2d9d60d73c038da39f82073ffadb459c96b901d66db7af11f59da58e0dd53e4"
|
||||
|
||||
Reference in New Issue
Block a user