From 409867a8e57bc493f01e87a236b6d565ef6dbdaa Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 27 Oct 2023 15:00:48 -0400 Subject: [PATCH] ota: Add support for legacy OTA metadata Android 11 OTAs use the same `payload.bin` format, but lack the `metadata.pb` protobuf representation of the OTA metadata. This commit adds support for parsing the legacy plain-text `metadata` format. Like before, the output files will still contain both the legacy and protobuf representations. Note that the legacy format allowed OEMs to specify arbitrary key/value pairs. These will be discarded during patching because they cannot be represented in the protobuf format, which is used in avbroot's internal representation. Issue: #195 Signed-off-by: Andrew Gunnerson --- README.md | 5 +- avbroot/src/cli/ota.rs | 39 +++++++---- avbroot/src/format/ota.rs | 138 ++++++++++++++++++++++++++++++++------ 3 files changed, 146 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 9587ed6..7d2d936 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,10 @@ avbroot applies two patches to the boot images: ## 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: +* The device must use modern (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. 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.pb` exists - * `META-INF/com/android/metadata` contains the line: `ota-type=AB` + * `META-INF/com/android/metadata` (Android 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. diff --git a/avbroot/src/cli/ota.rs b/avbroot/src/cli/ota.rs index a775cc4..da65d8c 100644 --- a/avbroot/src/cli/ota.rs +++ b/avbroot/src/cli/ota.rs @@ -734,12 +734,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. @@ -754,9 +749,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![]; @@ -777,19 +778,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; } _ => {} @@ -881,7 +896,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")?; diff --git a/avbroot/src/format/ota.rs b/avbroot/src/format/ota.rs index 42242a7..68b3ccb 100644 --- a/avbroot/src/format/ota.rs +++ b/avbroot/src/format/ota.rs @@ -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 = std::result::Result; +pub fn parse_protobuf_metadata(data: &[u8]) -> Result { + Ok(OtaMetadata::decode(data)?) +} + +/// Synthesize protobuf structure from legacy plain-text metadata. +pub fn parse_legacy_metadata(data: &str) -> Result { + 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 `=yes`. + let parse_yes = || match value { + "yes" => Ok(true), + _ => Err(unsupported()), + }; + let parse_list = || { + value + .split(LEGACY_SEP) + .map(|s| s.to_owned()) + .collect::>() + }; + + 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)> { - const SEP: &str = "|"; + use std::fmt::Write; let mut pairs = BTreeMap::::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)> { ); 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)> { } 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)> { pairs.extend(metadata.property_files.clone()); - let legacy_metadata = pairs - .into_iter() - .map(|(k, v)| format!("{k}={v}\n")) - .collect::(); + 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, next_offset: u64, - metadata_pb_raw: &[u8], + metadata: &OtaMetadata, payload_metadata_size: u64, ) -> Result { - 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();