blob: 4ff5027c27414269fd5d6bf3259f0577290da762 [file] [log] [blame]
Kelvin Zhangcff4d762020-07-29 16:37:51 -04001# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import copy
16import itertools
Yifan Hong125d0b62020-09-24 17:07:03 -070017import logging
Kelvin Zhangcff4d762020-07-29 16:37:51 -040018import os
Kelvin Zhangb9fdf2d2022-08-12 14:07:31 -070019import shutil
Kelvin Zhang25ab9982021-06-22 09:51:34 -040020import struct
Kelvin Zhangcff4d762020-07-29 16:37:51 -040021import zipfile
22
Tianjiea2076132020-08-19 17:25:32 -070023import ota_metadata_pb2
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +000024import common
Kelvin Zhangcff4d762020-07-29 16:37:51 -040025from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
26 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
TJ Rhoades6f488e92022-05-01 22:16:22 -070027 SignFile, PARTITIONS_WITH_BUILD_PROP, PartitionBuildProps,
28 GetRamdiskFormat)
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +000029from payload_signer import PayloadSigner
30
Kelvin Zhangcff4d762020-07-29 16:37:51 -040031
Yifan Hong125d0b62020-09-24 17:07:03 -070032logger = logging.getLogger(__name__)
Kelvin Zhang2e417382020-08-20 11:33:11 -040033
34OPTIONS.no_signing = False
35OPTIONS.force_non_ab = False
36OPTIONS.wipe_user_data = False
37OPTIONS.downgrade = False
38OPTIONS.key_passwords = {}
39OPTIONS.package_key = None
40OPTIONS.incremental_source = None
41OPTIONS.retrofit_dynamic_partitions = False
42OPTIONS.output_metadata_path = None
43OPTIONS.boot_variable_file = None
44
Kelvin Zhangcff4d762020-07-29 16:37:51 -040045METADATA_NAME = 'META-INF/com/android/metadata'
Tianjiea2076132020-08-19 17:25:32 -070046METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb'
Kelvin Zhangcff4d762020-07-29 16:37:51 -040047UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
Kelvin Zhang05ff7052021-02-10 09:13:26 -050048SECURITY_PATCH_LEVEL_PROP_NAME = "ro.build.version.security_patch"
49
Kelvin Zhangcff4d762020-07-29 16:37:51 -040050
Kelvin Zhangcff4d762020-07-29 16:37:51 -040051def FinalizeMetadata(metadata, input_file, output_file, needed_property_files):
52 """Finalizes the metadata and signs an A/B OTA package.
53
54 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
55 that contains the offsets and sizes for the ZIP entries. An example
56 property-files string is as follows.
57
58 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
59
60 OTA server can pass down this string, in addition to the package URL, to the
61 system update client. System update client can then fetch individual ZIP
62 entries (ZIP_STORED) directly at the given offset of the URL.
63
64 Args:
65 metadata: The metadata dict for the package.
66 input_file: The input ZIP filename that doesn't contain the package METADATA
67 entry yet.
68 output_file: The final output ZIP filename.
69 needed_property_files: The list of PropertyFiles' to be generated.
70 """
71
72 def ComputeAllPropertyFiles(input_file, needed_property_files):
73 # Write the current metadata entry with placeholders.
Kelvin Zhang928c2342020-09-22 16:15:57 -040074 with zipfile.ZipFile(input_file, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040075 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070076 metadata.property_files[property_files.name] = property_files.Compute(
77 input_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040078 namelist = input_zip.namelist()
79
Tianjiea2076132020-08-19 17:25:32 -070080 if METADATA_NAME in namelist or METADATA_PROTO_NAME in namelist:
81 ZipDelete(input_file, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhang928c2342020-09-22 16:15:57 -040082 output_zip = zipfile.ZipFile(input_file, 'a', allowZip64=True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040083 WriteMetadata(metadata, output_zip)
84 ZipClose(output_zip)
85
86 if OPTIONS.no_signing:
87 return input_file
88
89 prelim_signing = MakeTempFile(suffix='.zip')
90 SignOutput(input_file, prelim_signing)
91 return prelim_signing
92
93 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
Kelvin Zhang928c2342020-09-22 16:15:57 -040094 with zipfile.ZipFile(prelim_signing, allowZip64=True) as prelim_signing_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040095 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070096 metadata.property_files[property_files.name] = property_files.Finalize(
97 prelim_signing_zip,
98 len(metadata.property_files[property_files.name]))
Kelvin Zhangcff4d762020-07-29 16:37:51 -040099
100 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
101 # entries, as well as padding the entry headers. We do a preliminary signing
102 # (with an incomplete metadata entry) to allow that to happen. Then compute
103 # the ZIP entry offsets, write back the final metadata and do the final
104 # signing.
105 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
106 try:
107 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
108 except PropertyFiles.InsufficientSpaceException:
109 # Even with the preliminary signing, the entry orders may change
110 # dramatically, which leads to insufficiently reserved space during the
111 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
112 # preliminary signing works, based on the already ordered ZIP entries, to
113 # address the issue.
114 prelim_signing = ComputeAllPropertyFiles(
115 prelim_signing, needed_property_files)
116 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
117
118 # Replace the METADATA entry.
Tianjiea2076132020-08-19 17:25:32 -0700119 ZipDelete(prelim_signing, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhang928c2342020-09-22 16:15:57 -0400120 output_zip = zipfile.ZipFile(prelim_signing, 'a', allowZip64=True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400121 WriteMetadata(metadata, output_zip)
122 ZipClose(output_zip)
123
124 # Re-sign the package after updating the metadata entry.
125 if OPTIONS.no_signing:
Kelvin Zhangb9fdf2d2022-08-12 14:07:31 -0700126 shutil.copy(prelim_signing, output_file)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400127 else:
128 SignOutput(prelim_signing, output_file)
129
130 # Reopen the final signed zip to double check the streaming metadata.
Kelvin Zhang928c2342020-09-22 16:15:57 -0400131 with zipfile.ZipFile(output_file, allowZip64=True) as output_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400132 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700133 property_files.Verify(
134 output_zip, metadata.property_files[property_files.name].strip())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400135
136 # If requested, dump the metadata to a separate file.
137 output_metadata_path = OPTIONS.output_metadata_path
138 if output_metadata_path:
139 WriteMetadata(metadata, output_metadata_path)
140
141
Tianjiea2076132020-08-19 17:25:32 -0700142def WriteMetadata(metadata_proto, output):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400143 """Writes the metadata to the zip archive or a file.
144
145 Args:
Tianjiea2076132020-08-19 17:25:32 -0700146 metadata_proto: The metadata protobuf for the package.
147 output: A ZipFile object or a string of the output file path. If a string
148 path is given, the metadata in the protobuf format will be written to
149 {output}.pb, e.g. ota_metadata.pb
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400150 """
151
Tianjiea2076132020-08-19 17:25:32 -0700152 metadata_dict = BuildLegacyOtaMetadata(metadata_proto)
153 legacy_metadata = "".join(["%s=%s\n" % kv for kv in
154 sorted(metadata_dict.items())])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400155 if isinstance(output, zipfile.ZipFile):
Tianjiea2076132020-08-19 17:25:32 -0700156 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(),
157 compress_type=zipfile.ZIP_STORED)
158 ZipWriteStr(output, METADATA_NAME, legacy_metadata,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400159 compress_type=zipfile.ZIP_STORED)
160 return
161
Cole Faustb820bcd2021-10-28 13:59:48 -0700162 with open('{}.pb'.format(output), 'wb') as f:
Tianjiea2076132020-08-19 17:25:32 -0700163 f.write(metadata_proto.SerializeToString())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400164 with open(output, 'w') as f:
Tianjiea2076132020-08-19 17:25:32 -0700165 f.write(legacy_metadata)
166
167
168def UpdateDeviceState(device_state, build_info, boot_variable_values,
169 is_post_build):
170 """Update the fields of the DeviceState proto with build info."""
171
Tianjie2bb14862020-08-28 16:24:34 -0700172 def UpdatePartitionStates(partition_states):
173 """Update the per-partition state according to its build.prop"""
Kelvin Zhang39aea442020-08-17 11:04:25 -0400174 if not build_info.is_ab:
175 return
Tianjie2bb14862020-08-28 16:24:34 -0700176 build_info_set = ComputeRuntimeBuildInfos(build_info,
177 boot_variable_values)
Kelvin Zhang39aea442020-08-17 11:04:25 -0400178 assert "ab_partitions" in build_info.info_dict,\
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500179 "ab_partitions property required for ab update."
Kelvin Zhang39aea442020-08-17 11:04:25 -0400180 ab_partitions = set(build_info.info_dict.get("ab_partitions"))
181
182 # delta_generator will error out on unused timestamps,
183 # so only generate timestamps for dynamic partitions
184 # used in OTA update.
Yifan Hong5057b952021-01-07 14:09:57 -0800185 for partition in sorted(set(PARTITIONS_WITH_BUILD_PROP) & ab_partitions):
Tianjie2bb14862020-08-28 16:24:34 -0700186 partition_prop = build_info.info_dict.get(
187 '{}.build.prop'.format(partition))
188 # Skip if the partition is missing, or it doesn't have a build.prop
189 if not partition_prop or not partition_prop.build_props:
190 continue
191
192 partition_state = partition_states.add()
193 partition_state.partition_name = partition
194 # Update the partition's runtime device names and fingerprints
195 partition_devices = set()
196 partition_fingerprints = set()
197 for runtime_build_info in build_info_set:
198 partition_devices.add(
199 runtime_build_info.GetPartitionBuildProp('ro.product.device',
200 partition))
201 partition_fingerprints.add(
202 runtime_build_info.GetPartitionFingerprint(partition))
203
204 partition_state.device.extend(sorted(partition_devices))
205 partition_state.build.extend(sorted(partition_fingerprints))
206
207 # TODO(xunchang) set the boot image's version with kmi. Note the boot
208 # image doesn't have a file map.
209 partition_state.version = build_info.GetPartitionBuildProp(
210 'ro.build.date.utc', partition)
211
212 # TODO(xunchang), we can save a call to ComputeRuntimeBuildInfos.
Tianjiea2076132020-08-19 17:25:32 -0700213 build_devices, build_fingerprints = \
214 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values)
215 device_state.device.extend(sorted(build_devices))
216 device_state.build.extend(sorted(build_fingerprints))
217 device_state.build_incremental = build_info.GetBuildProp(
218 'ro.build.version.incremental')
219
Tianjie2bb14862020-08-28 16:24:34 -0700220 UpdatePartitionStates(device_state.partition_state)
Tianjiea2076132020-08-19 17:25:32 -0700221
222 if is_post_build:
223 device_state.sdk_level = build_info.GetBuildProp(
224 'ro.build.version.sdk')
225 device_state.security_patch_level = build_info.GetBuildProp(
226 'ro.build.version.security_patch')
227 # Use the actual post-timestamp, even for a downgrade case.
228 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc'))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400229
230
231def GetPackageMetadata(target_info, source_info=None):
Tianjiea2076132020-08-19 17:25:32 -0700232 """Generates and returns the metadata proto.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400233
Tianjiea2076132020-08-19 17:25:32 -0700234 It generates a ota_metadata protobuf that contains the info to be written
235 into an OTA package (META-INF/com/android/metadata.pb). It also handles the
236 detection of downgrade / data wipe based on the global options.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400237
238 Args:
239 target_info: The BuildInfo instance that holds the target build info.
240 source_info: The BuildInfo instance that holds the source build info, or
241 None if generating full OTA.
242
243 Returns:
Tianjiea2076132020-08-19 17:25:32 -0700244 A protobuf to be written into package metadata entry.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400245 """
246 assert isinstance(target_info, BuildInfo)
247 assert source_info is None or isinstance(source_info, BuildInfo)
248
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400249 boot_variable_values = {}
250 if OPTIONS.boot_variable_file:
251 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
252 for key, values in d.items():
253 boot_variable_values[key] = [val.strip() for val in values.split(',')]
254
Tianjiea2076132020-08-19 17:25:32 -0700255 metadata_proto = ota_metadata_pb2.OtaMetadata()
256 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can
257 # consider skipping them if they aren't used by clients.
258 UpdateDeviceState(metadata_proto.postcondition, target_info,
259 boot_variable_values, True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400260
261 if target_info.is_ab and not OPTIONS.force_non_ab:
Tianjiea2076132020-08-19 17:25:32 -0700262 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB
263 metadata_proto.required_cache = 0
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400264 else:
Tianjiea2076132020-08-19 17:25:32 -0700265 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK
266 # cache requirement will be updated by the non-A/B codes.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400267
268 if OPTIONS.wipe_user_data:
Tianjiea2076132020-08-19 17:25:32 -0700269 metadata_proto.wipe = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400270
271 if OPTIONS.retrofit_dynamic_partitions:
Tianjiea2076132020-08-19 17:25:32 -0700272 metadata_proto.retrofit_dynamic_partitions = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400273
274 is_incremental = source_info is not None
275 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700276 UpdateDeviceState(metadata_proto.precondition, source_info,
277 boot_variable_values, False)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400278 else:
Tianjiea2076132020-08-19 17:25:32 -0700279 metadata_proto.precondition.device.extend(
280 metadata_proto.postcondition.device)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400281
282 # Detect downgrades and set up downgrade flags accordingly.
283 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700284 HandleDowngradeMetadata(metadata_proto, target_info, source_info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400285
Tianjiea2076132020-08-19 17:25:32 -0700286 return metadata_proto
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400287
288
Tianjiea2076132020-08-19 17:25:32 -0700289def BuildLegacyOtaMetadata(metadata_proto):
290 """Converts the metadata proto to a legacy metadata dict.
291
292 This metadata dict is used to build the legacy metadata text file for
293 backward compatibility. We won't add new keys to the legacy metadata format.
294 If new information is needed, we should add it as a new field in OtaMetadata
295 proto definition.
296 """
297
298 separator = '|'
299
300 metadata_dict = {}
301 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB:
302 metadata_dict['ota-type'] = 'AB'
303 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK:
304 metadata_dict['ota-type'] = 'BLOCK'
305 if metadata_proto.wipe:
306 metadata_dict['ota-wipe'] = 'yes'
307 if metadata_proto.retrofit_dynamic_partitions:
308 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes'
309 if metadata_proto.downgrade:
310 metadata_dict['ota-downgrade'] = 'yes'
311
312 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache)
313
314 post_build = metadata_proto.postcondition
315 metadata_dict['post-build'] = separator.join(post_build.build)
316 metadata_dict['post-build-incremental'] = post_build.build_incremental
317 metadata_dict['post-sdk-level'] = post_build.sdk_level
318 metadata_dict['post-security-patch-level'] = post_build.security_patch_level
319 metadata_dict['post-timestamp'] = str(post_build.timestamp)
320
321 pre_build = metadata_proto.precondition
322 metadata_dict['pre-device'] = separator.join(pre_build.device)
323 # incremental updates
324 if len(pre_build.build) != 0:
325 metadata_dict['pre-build'] = separator.join(pre_build.build)
326 metadata_dict['pre-build-incremental'] = pre_build.build_incremental
327
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500328 if metadata_proto.spl_downgrade:
329 metadata_dict['spl-downgrade'] = 'yes'
Tianjiea2076132020-08-19 17:25:32 -0700330 metadata_dict.update(metadata_proto.property_files)
331
332 return metadata_dict
333
334
335def HandleDowngradeMetadata(metadata_proto, target_info, source_info):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400336 # Only incremental OTAs are allowed to reach here.
337 assert OPTIONS.incremental_source is not None
338
339 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
340 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
341 is_downgrade = int(post_timestamp) < int(pre_timestamp)
342
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500343 if OPTIONS.spl_downgrade:
344 metadata_proto.spl_downgrade = True
345
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400346 if OPTIONS.downgrade:
347 if not is_downgrade:
348 raise RuntimeError(
349 "--downgrade or --override_timestamp specified but no downgrade "
350 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
Tianjiea2076132020-08-19 17:25:32 -0700351 metadata_proto.downgrade = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400352 else:
353 if is_downgrade:
354 raise RuntimeError(
355 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
356 "Need to specify --override_timestamp OR --downgrade to allow "
357 "building the incremental." % (pre_timestamp, post_timestamp))
358
359
Tianjie2bb14862020-08-28 16:24:34 -0700360def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values):
361 """Returns a set of build info objects that may exist during runtime."""
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400362
Tianjie2bb14862020-08-28 16:24:34 -0700363 build_info_set = {default_build_info}
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400364 if not boot_variable_values:
Tianjie2bb14862020-08-28 16:24:34 -0700365 return build_info_set
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400366
367 # Calculate all possible combinations of the values for the boot variables.
368 keys = boot_variable_values.keys()
369 value_list = boot_variable_values.values()
370 combinations = [dict(zip(keys, values))
371 for values in itertools.product(*value_list)]
372 for placeholder_values in combinations:
373 # Reload the info_dict as some build properties may change their values
374 # based on the value of ro.boot* properties.
Tianjie2bb14862020-08-28 16:24:34 -0700375 info_dict = copy.deepcopy(default_build_info.info_dict)
Yifan Hong5057b952021-01-07 14:09:57 -0800376 for partition in PARTITIONS_WITH_BUILD_PROP:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400377 partition_prop_key = "{}.build.prop".format(partition)
378 input_file = info_dict[partition_prop_key].input_file
TJ Rhoades6f488e92022-05-01 22:16:22 -0700379 ramdisk = GetRamdiskFormat(info_dict)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400380 if isinstance(input_file, zipfile.ZipFile):
Kelvin Zhang928c2342020-09-22 16:15:57 -0400381 with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400382 info_dict[partition_prop_key] = \
383 PartitionBuildProps.FromInputFile(input_zip, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700384 placeholder_values,
385 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400386 else:
387 info_dict[partition_prop_key] = \
388 PartitionBuildProps.FromInputFile(input_file, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700389 placeholder_values,
390 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400391 info_dict["build.prop"] = info_dict["system.build.prop"]
Tianjie2bb14862020-08-28 16:24:34 -0700392 build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400393
Tianjie2bb14862020-08-28 16:24:34 -0700394 return build_info_set
395
396
397def CalculateRuntimeDevicesAndFingerprints(default_build_info,
398 boot_variable_values):
399 """Returns a tuple of sets for runtime devices and fingerprints"""
400
401 device_names = set()
402 fingerprints = set()
403 build_info_set = ComputeRuntimeBuildInfos(default_build_info,
404 boot_variable_values)
405 for runtime_build_info in build_info_set:
406 device_names.add(runtime_build_info.device)
407 fingerprints.add(runtime_build_info.fingerprint)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400408 return device_names, fingerprints
409
410
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400411def GetZipEntryOffset(zfp, entry_info):
412 """Get offset to a beginning of a particular zip entry
413 Args:
414 fp: zipfile.ZipFile
415 entry_info: zipfile.ZipInfo
416
417 Returns:
418 (offset, size) tuple
419 """
420 # Don't use len(entry_info.extra). Because that returns size of extra
421 # fields in central directory. We need to look at local file directory,
422 # as these two might have different sizes.
423
424 # We cannot work with zipfile.ZipFile instances, we need a |fp| for the underlying file.
425 zfp = zfp.fp
426 zfp.seek(entry_info.header_offset)
427 data = zfp.read(zipfile.sizeFileHeader)
428 fheader = struct.unpack(zipfile.structFileHeader, data)
429 # Last two fields of local file header are filename length and
430 # extra length
431 filename_len = fheader[-2]
432 extra_len = fheader[-1]
433 offset = entry_info.header_offset
434 offset += zipfile.sizeFileHeader
435 offset += filename_len + extra_len
436 size = entry_info.file_size
437 return (offset, size)
438
439
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400440class PropertyFiles(object):
441 """A class that computes the property-files string for an OTA package.
442
443 A property-files string is a comma-separated string that contains the
444 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
445 can be fetched directly with the package URL along with the offset/size info.
446 These strings can be used for streaming A/B OTAs, or allowing an updater to
447 download package metadata entry directly, without paying the cost of
448 downloading entire package.
449
450 Computing the final property-files string requires two passes. Because doing
451 the whole package signing (with signapk.jar) will possibly reorder the ZIP
452 entries, which may in turn invalidate earlier computed ZIP entry offset/size
453 values.
454
455 This class provides functions to be called for each pass. The general flow is
456 as follows.
457
458 property_files = PropertyFiles()
459 # The first pass, which writes placeholders before doing initial signing.
460 property_files.Compute()
461 SignOutput()
462
463 # The second pass, by replacing the placeholders with actual data.
464 property_files.Finalize()
465 SignOutput()
466
467 And the caller can additionally verify the final result.
468
469 property_files.Verify()
470 """
471
472 def __init__(self):
473 self.name = None
474 self.required = ()
475 self.optional = ()
476
477 def Compute(self, input_zip):
478 """Computes and returns a property-files string with placeholders.
479
480 We reserve extra space for the offset and size of the metadata entry itself,
481 although we don't know the final values until the package gets signed.
482
483 Args:
484 input_zip: The input ZIP file.
485
486 Returns:
487 A string with placeholders for the metadata offset/size info, e.g.
488 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
489 """
490 return self.GetPropertyFilesString(input_zip, reserve_space=True)
491
492 class InsufficientSpaceException(Exception):
493 pass
494
495 def Finalize(self, input_zip, reserved_length):
496 """Finalizes a property-files string with actual METADATA offset/size info.
497
498 The input ZIP file has been signed, with the ZIP entries in the desired
499 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
500 the ZIP entry offsets and construct the property-files string with actual
501 data. Note that during this process, we must pad the property-files string
502 to the reserved length, so that the METADATA entry size remains the same.
503 Otherwise the entries' offsets and sizes may change again.
504
505 Args:
506 input_zip: The input ZIP file.
507 reserved_length: The reserved length of the property-files string during
508 the call to Compute(). The final string must be no more than this
509 size.
510
511 Returns:
512 A property-files string including the metadata offset/size info, e.g.
513 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
514
515 Raises:
516 InsufficientSpaceException: If the reserved length is insufficient to hold
517 the final string.
518 """
519 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
520 if len(result) > reserved_length:
521 raise self.InsufficientSpaceException(
522 'Insufficient reserved space: reserved={}, actual={}'.format(
523 reserved_length, len(result)))
524
525 result += ' ' * (reserved_length - len(result))
526 return result
527
528 def Verify(self, input_zip, expected):
529 """Verifies the input ZIP file contains the expected property-files string.
530
531 Args:
532 input_zip: The input ZIP file.
533 expected: The property-files string that's computed from Finalize().
534
535 Raises:
536 AssertionError: On finding a mismatch.
537 """
538 actual = self.GetPropertyFilesString(input_zip)
539 assert actual == expected, \
540 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
541
542 def GetPropertyFilesString(self, zip_file, reserve_space=False):
543 """
544 Constructs the property-files string per request.
545
546 Args:
547 zip_file: The input ZIP file.
548 reserved_length: The reserved length of the property-files string.
549
550 Returns:
551 A property-files string including the metadata offset/size info, e.g.
552 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
553 """
554
555 def ComputeEntryOffsetSize(name):
556 """Computes the zip entry offset and size."""
557 info = zip_file.getinfo(name)
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400558 (offset, size) = GetZipEntryOffset(zip_file, info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400559 return '%s:%d:%d' % (os.path.basename(name), offset, size)
560
561 tokens = []
562 tokens.extend(self._GetPrecomputed(zip_file))
563 for entry in self.required:
564 tokens.append(ComputeEntryOffsetSize(entry))
565 for entry in self.optional:
566 if entry in zip_file.namelist():
567 tokens.append(ComputeEntryOffsetSize(entry))
568
569 # 'META-INF/com/android/metadata' is required. We don't know its actual
570 # offset and length (as well as the values for other entries). So we reserve
571 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
572 # the space for metadata entry. Because 'offset' allows a max of 10-digit
573 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
574 # reserved space serves the metadata entry only.
575 if reserve_space:
576 tokens.append('metadata:' + ' ' * 15)
Tianjiea2076132020-08-19 17:25:32 -0700577 tokens.append('metadata.pb:' + ' ' * 15)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400578 else:
579 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
Luca Stefanib6075c52021-11-03 17:10:54 +0100580 if METADATA_PROTO_NAME in zip_file.namelist():
Kelvin Zhangfa928692022-08-16 17:01:53 +0000581 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400582
583 return ','.join(tokens)
584
585 def _GetPrecomputed(self, input_zip):
586 """Computes the additional tokens to be included into the property-files.
587
588 This applies to tokens without actual ZIP entries, such as
589 payload_metadata.bin. We want to expose the offset/size to updaters, so
590 that they can download the payload metadata directly with the info.
591
592 Args:
593 input_zip: The input zip file.
594
595 Returns:
596 A list of strings (tokens) to be added to the property-files string.
597 """
598 # pylint: disable=no-self-use
599 # pylint: disable=unused-argument
600 return []
601
602
603def SignOutput(temp_zip_name, output_zip_name):
604 pw = OPTIONS.key_passwords[OPTIONS.package_key]
605
606 SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
607 whole_file=True)
Tianjiea5fca032021-06-01 22:06:28 -0700608
609
610def ConstructOtaApexInfo(target_zip, source_file=None):
611 """If applicable, add the source version to the apex info."""
612
613 def _ReadApexInfo(input_zip):
614 if "META/apex_info.pb" not in input_zip.namelist():
615 logger.warning("target_file doesn't contain apex_info.pb %s", input_zip)
616 return None
617
618 with input_zip.open("META/apex_info.pb", "r") as zfp:
619 return zfp.read()
620
621 target_apex_string = _ReadApexInfo(target_zip)
622 # Return early if the target apex info doesn't exist or is empty.
623 if not target_apex_string:
624 return target_apex_string
625
626 # If the source apex info isn't available, just return the target info
627 if not source_file:
628 return target_apex_string
629
630 with zipfile.ZipFile(source_file, "r", allowZip64=True) as source_zip:
631 source_apex_string = _ReadApexInfo(source_zip)
632 if not source_apex_string:
633 return target_apex_string
634
635 source_apex_proto = ota_metadata_pb2.ApexMetadata()
636 source_apex_proto.ParseFromString(source_apex_string)
637 source_apex_versions = {apex.package_name: apex.version for apex in
638 source_apex_proto.apex_info}
639
640 # If the apex package is available in the source build, initialize the source
641 # apex version.
642 target_apex_proto = ota_metadata_pb2.ApexMetadata()
643 target_apex_proto.ParseFromString(target_apex_string)
644 for target_apex in target_apex_proto.apex_info:
645 name = target_apex.package_name
646 if name in source_apex_versions:
647 target_apex.source_version = source_apex_versions[name]
648
649 return target_apex_proto.SerializeToString()
Kelvin Zhang410bb382022-01-06 09:15:54 -0800650
651
Kelvin Zhangf2728d62022-01-10 11:42:36 -0800652def IsLz4diffCompatible(source_file: str, target_file: str):
653 """Check whether lz4diff versions in two builds are compatible
654
655 Args:
656 source_file: Path to source build's target_file.zip
657 target_file: Path to target build's target_file.zip
658
659 Returns:
660 bool true if and only if lz4diff versions are compatible
661 """
662 if source_file is None or target_file is None:
663 return False
664 # Right now we enable lz4diff as long as source build has liblz4.so.
665 # In the future we might introduce version system to lz4diff as well.
666 if zipfile.is_zipfile(source_file):
667 with zipfile.ZipFile(source_file, "r") as zfp:
668 return "META/liblz4.so" in zfp.namelist()
669 else:
670 assert os.path.isdir(source_file)
671 return os.path.exists(os.path.join(source_file, "META", "liblz4.so"))
672
673
Kelvin Zhang410bb382022-01-06 09:15:54 -0800674def IsZucchiniCompatible(source_file: str, target_file: str):
675 """Check whether zucchini versions in two builds are compatible
676
677 Args:
678 source_file: Path to source build's target_file.zip
679 target_file: Path to target build's target_file.zip
680
681 Returns:
682 bool true if and only if zucchini versions are compatible
683 """
684 if source_file is None or target_file is None:
685 return False
686 assert os.path.exists(source_file)
687 assert os.path.exists(target_file)
688
689 assert zipfile.is_zipfile(source_file) or os.path.isdir(source_file)
690 assert zipfile.is_zipfile(target_file) or os.path.isdir(target_file)
691 _ZUCCHINI_CONFIG_ENTRY_NAME = "META/zucchini_config.txt"
692
693 def ReadEntry(path, entry):
694 # Read an entry inside a .zip file or extracted dir of .zip file
695 if zipfile.is_zipfile(path):
696 with zipfile.ZipFile(path, "r", allowZip64=True) as zfp:
697 if entry in zfp.namelist():
698 return zfp.read(entry).decode()
699 else:
700 entry_path = os.path.join(entry, path)
701 if os.path.exists(entry_path):
702 with open(entry_path, "r") as fp:
703 return fp.read()
HÃ¥kan Kvist3db1ef62022-05-03 10:19:41 +0200704 return False
705 sourceEntry = ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
706 targetEntry = ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
707 return sourceEntry and targetEntry and sourceEntry == targetEntry
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000708
709
Kelvin Zhangfa928692022-08-16 17:01:53 +0000710class PayloadGenerator(object):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000711 """Manages the creation and the signing of an A/B OTA Payload."""
712
713 PAYLOAD_BIN = 'payload.bin'
714 PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
715 SECONDARY_PAYLOAD_BIN = 'secondary/payload.bin'
716 SECONDARY_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt'
717
718 def __init__(self, secondary=False):
719 """Initializes a Payload instance.
720
721 Args:
722 secondary: Whether it's generating a secondary payload (default: False).
723 """
724 self.payload_file = None
725 self.payload_properties = None
726 self.secondary = secondary
727
728 def _Run(self, cmd): # pylint: disable=no-self-use
729 # Don't pipe (buffer) the output if verbose is set. Let
730 # brillo_update_payload write to stdout/stderr directly, so its progress can
731 # be monitored.
732 if OPTIONS.verbose:
733 common.RunAndCheckOutput(cmd, stdout=None, stderr=None)
734 else:
735 common.RunAndCheckOutput(cmd)
736
737 def Generate(self, target_file, source_file=None, additional_args=None):
738 """Generates a payload from the given target-files zip(s).
739
740 Args:
741 target_file: The filename of the target build target-files zip.
742 source_file: The filename of the source build target-files zip; or None if
743 generating a full OTA.
744 additional_args: A list of additional args that should be passed to
745 brillo_update_payload script; or None.
746 """
747 if additional_args is None:
748 additional_args = []
749
750 payload_file = common.MakeTempFile(prefix="payload-", suffix=".bin")
751 cmd = ["brillo_update_payload", "generate",
752 "--payload", payload_file,
753 "--target_image", target_file]
754 if source_file is not None:
755 cmd.extend(["--source_image", source_file])
756 if OPTIONS.disable_fec_computation:
757 cmd.extend(["--disable_fec_computation", "true"])
758 if OPTIONS.disable_verity_computation:
759 cmd.extend(["--disable_verity_computation", "true"])
760 cmd.extend(additional_args)
761 self._Run(cmd)
762
763 self.payload_file = payload_file
764 self.payload_properties = None
765
766 def Sign(self, payload_signer):
767 """Generates and signs the hashes of the payload and metadata.
768
769 Args:
770 payload_signer: A PayloadSigner() instance that serves the signing work.
771
772 Raises:
773 AssertionError: On any failure when calling brillo_update_payload script.
774 """
775 assert isinstance(payload_signer, PayloadSigner)
776
777 # 1. Generate hashes of the payload and metadata files.
778 payload_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
779 metadata_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
780 cmd = ["brillo_update_payload", "hash",
781 "--unsigned_payload", self.payload_file,
782 "--signature_size", str(payload_signer.maximum_signature_size),
783 "--metadata_hash_file", metadata_sig_file,
784 "--payload_hash_file", payload_sig_file]
785 self._Run(cmd)
786
787 # 2. Sign the hashes.
788 signed_payload_sig_file = payload_signer.Sign(payload_sig_file)
789 signed_metadata_sig_file = payload_signer.Sign(metadata_sig_file)
790
791 # 3. Insert the signatures back into the payload file.
792 signed_payload_file = common.MakeTempFile(prefix="signed-payload-",
793 suffix=".bin")
794 cmd = ["brillo_update_payload", "sign",
795 "--unsigned_payload", self.payload_file,
796 "--payload", signed_payload_file,
797 "--signature_size", str(payload_signer.maximum_signature_size),
798 "--metadata_signature_file", signed_metadata_sig_file,
799 "--payload_signature_file", signed_payload_sig_file]
800 self._Run(cmd)
801
802 # 4. Dump the signed payload properties.
803 properties_file = common.MakeTempFile(prefix="payload-properties-",
804 suffix=".txt")
805 cmd = ["brillo_update_payload", "properties",
806 "--payload", signed_payload_file,
807 "--properties_file", properties_file]
808 self._Run(cmd)
809
810 if self.secondary:
811 with open(properties_file, "a") as f:
812 f.write("SWITCH_SLOT_ON_REBOOT=0\n")
813
814 if OPTIONS.wipe_user_data:
815 with open(properties_file, "a") as f:
816 f.write("POWERWASH=1\n")
817
818 self.payload_file = signed_payload_file
819 self.payload_properties = properties_file
820
821 def WriteToZip(self, output_zip):
822 """Writes the payload to the given zip.
823
824 Args:
825 output_zip: The output ZipFile instance.
826 """
827 assert self.payload_file is not None
828 assert self.payload_properties is not None
829
830 if self.secondary:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000831 payload_arcname = PayloadGenerator.SECONDARY_PAYLOAD_BIN
832 payload_properties_arcname = PayloadGenerator.SECONDARY_PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000833 else:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000834 payload_arcname = PayloadGenerator.PAYLOAD_BIN
835 payload_properties_arcname = PayloadGenerator.PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000836
837 # Add the signed payload file and properties into the zip. In order to
838 # support streaming, we pack them as ZIP_STORED. So these entries can be
839 # read directly with the offset and length pairs.
840 common.ZipWrite(output_zip, self.payload_file, arcname=payload_arcname,
841 compress_type=zipfile.ZIP_STORED)
842 common.ZipWrite(output_zip, self.payload_properties,
843 arcname=payload_properties_arcname,
844 compress_type=zipfile.ZIP_STORED)
845
846
847class StreamingPropertyFiles(PropertyFiles):
848 """A subclass for computing the property-files for streaming A/B OTAs."""
849
850 def __init__(self):
851 super(StreamingPropertyFiles, self).__init__()
852 self.name = 'ota-streaming-property-files'
853 self.required = (
854 # payload.bin and payload_properties.txt must exist.
855 'payload.bin',
856 'payload_properties.txt',
857 )
858 self.optional = (
859 # apex_info.pb isn't directly used in the update flow
860 'apex_info.pb',
861 # care_map is available only if dm-verity is enabled.
862 'care_map.pb',
863 'care_map.txt',
864 # compatibility.zip is available only if target supports Treble.
865 'compatibility.zip',
866 )
867
868
869class AbOtaPropertyFiles(StreamingPropertyFiles):
870 """The property-files for A/B OTA that includes payload_metadata.bin info.
871
872 Since P, we expose one more token (aka property-file), in addition to the ones
873 for streaming A/B OTA, for a virtual entry of 'payload_metadata.bin'.
874 'payload_metadata.bin' is the header part of a payload ('payload.bin'), which
875 doesn't exist as a separate ZIP entry, but can be used to verify if the
876 payload can be applied on the given device.
877
878 For backward compatibility, we keep both of the 'ota-streaming-property-files'
879 and the newly added 'ota-property-files' in P. The new token will only be
880 available in 'ota-property-files'.
881 """
882
883 def __init__(self):
884 super(AbOtaPropertyFiles, self).__init__()
885 self.name = 'ota-property-files'
886
887 def _GetPrecomputed(self, input_zip):
888 offset, size = self._GetPayloadMetadataOffsetAndSize(input_zip)
889 return ['payload_metadata.bin:{}:{}'.format(offset, size)]
890
891 @staticmethod
892 def _GetPayloadMetadataOffsetAndSize(input_zip):
893 """Computes the offset and size of the payload metadata for a given package.
894
895 (From system/update_engine/update_metadata.proto)
896 A delta update file contains all the deltas needed to update a system from
897 one specific version to another specific version. The update format is
898 represented by this struct pseudocode:
899
900 struct delta_update_file {
901 char magic[4] = "CrAU";
902 uint64 file_format_version;
903 uint64 manifest_size; // Size of protobuf DeltaArchiveManifest
904
905 // Only present if format_version > 1:
906 uint32 metadata_signature_size;
907
908 // The Bzip2 compressed DeltaArchiveManifest
909 char manifest[metadata_signature_size];
910
911 // The signature of the metadata (from the beginning of the payload up to
912 // this location, not including the signature itself). This is a
913 // serialized Signatures message.
914 char medatada_signature_message[metadata_signature_size];
915
916 // Data blobs for files, no specific format. The specific offset
917 // and length of each data blob is recorded in the DeltaArchiveManifest.
918 struct {
919 char data[];
920 } blobs[];
921
922 // These two are not signed:
923 uint64 payload_signatures_message_size;
924 char payload_signatures_message[];
925 };
926
927 'payload-metadata.bin' contains all the bytes from the beginning of the
928 payload, till the end of 'medatada_signature_message'.
929 """
930 payload_info = input_zip.getinfo('payload.bin')
931 (payload_offset, payload_size) = GetZipEntryOffset(input_zip, payload_info)
932
933 # Read the underlying raw zipfile at specified offset
934 payload_fp = input_zip.fp
935 payload_fp.seek(payload_offset)
936 header_bin = payload_fp.read(24)
937
938 # network byte order (big-endian)
939 header = struct.unpack("!IQQL", header_bin)
940
941 # 'CrAU'
942 magic = header[0]
943 assert magic == 0x43724155, "Invalid magic: {:x}, computed offset {}" \
944 .format(magic, payload_offset)
945
946 manifest_size = header[2]
947 metadata_signature_size = header[3]
948 metadata_total = 24 + manifest_size + metadata_signature_size
949 assert metadata_total < payload_size
950
951 return (payload_offset, metadata_total)