1# 2# Copyright (C) 2013 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16 17"""Tools for reading, verifying and applying Chrome OS update payloads.""" 18 19from __future__ import print_function 20 21import hashlib 22import struct 23 24from update_payload import applier 25from update_payload import checker 26from update_payload import common 27from update_payload import update_metadata_pb2 28from update_payload.error import PayloadError 29 30 31# 32# Helper functions. 33# 34def _ReadInt(file_obj, size, is_unsigned, hasher=None): 35 """Reads a binary-encoded integer from a file. 36 37 It will do the correct conversion based on the reported size and whether or 38 not a signed number is expected. Assumes a network (big-endian) byte 39 ordering. 40 41 Args: 42 file_obj: a file object 43 size: the integer size in bytes (2, 4 or 8) 44 is_unsigned: whether it is signed or not 45 hasher: an optional hasher to pass the value through 46 47 Returns: 48 An "unpacked" (Python) integer value. 49 50 Raises: 51 PayloadError if an read error occurred. 52 """ 53 return struct.unpack(common.IntPackingFmtStr(size, is_unsigned), 54 common.Read(file_obj, size, hasher=hasher))[0] 55 56 57# 58# Update payload. 59# 60class Payload(object): 61 """Chrome OS update payload processor.""" 62 63 class _PayloadHeader(object): 64 """Update payload header struct.""" 65 66 # Header constants; sizes are in bytes. 67 _MAGIC = 'CrAU' 68 _VERSION_SIZE = 8 69 _MANIFEST_LEN_SIZE = 8 70 _METADATA_SIGNATURE_LEN_SIZE = 4 71 72 def __init__(self): 73 self.version = None 74 self.manifest_len = None 75 self.metadata_signature_len = None 76 self.size = None 77 78 def ReadFromPayload(self, payload_file, hasher=None): 79 """Reads the payload header from a file. 80 81 Reads the payload header from the |payload_file| and updates the |hasher| 82 if one is passed. The parsed header is stored in the _PayloadHeader 83 instance attributes. 84 85 Args: 86 payload_file: a file object 87 hasher: an optional hasher to pass the value through 88 89 Returns: 90 None. 91 92 Raises: 93 PayloadError if a read error occurred or the header is invalid. 94 """ 95 # Verify magic 96 magic = common.Read(payload_file, len(self._MAGIC), hasher=hasher) 97 if magic != self._MAGIC: 98 raise PayloadError('invalid payload magic: %s' % magic) 99 100 self.version = _ReadInt(payload_file, self._VERSION_SIZE, True, 101 hasher=hasher) 102 self.manifest_len = _ReadInt(payload_file, self._MANIFEST_LEN_SIZE, True, 103 hasher=hasher) 104 self.size = (len(self._MAGIC) + self._VERSION_SIZE + 105 self._MANIFEST_LEN_SIZE) 106 self.metadata_signature_len = 0 107 108 if self.version == common.BRILLO_MAJOR_PAYLOAD_VERSION: 109 self.size += self._METADATA_SIGNATURE_LEN_SIZE 110 self.metadata_signature_len = _ReadInt( 111 payload_file, self._METADATA_SIGNATURE_LEN_SIZE, True, 112 hasher=hasher) 113 114 115 def __init__(self, payload_file, payload_file_offset=0): 116 """Initialize the payload object. 117 118 Args: 119 payload_file: update payload file object open for reading 120 payload_file_offset: the offset of the actual payload 121 """ 122 self.payload_file = payload_file 123 self.payload_file_offset = payload_file_offset 124 self.manifest_hasher = None 125 self.is_init = False 126 self.header = None 127 self.manifest = None 128 self.data_offset = None 129 self.metadata_signature = None 130 self.metadata_size = None 131 132 def _ReadHeader(self): 133 """Reads and returns the payload header. 134 135 Returns: 136 A payload header object. 137 138 Raises: 139 PayloadError if a read error occurred. 140 """ 141 header = self._PayloadHeader() 142 header.ReadFromPayload(self.payload_file, self.manifest_hasher) 143 return header 144 145 def _ReadManifest(self): 146 """Reads and returns the payload manifest. 147 148 Returns: 149 A string containing the payload manifest in binary form. 150 151 Raises: 152 PayloadError if a read error occurred. 153 """ 154 if not self.header: 155 raise PayloadError('payload header not present') 156 157 return common.Read(self.payload_file, self.header.manifest_len, 158 hasher=self.manifest_hasher) 159 160 def _ReadMetadataSignature(self): 161 """Reads and returns the metadata signatures. 162 163 Returns: 164 A string containing the metadata signatures protobuf in binary form or 165 an empty string if no metadata signature found in the payload. 166 167 Raises: 168 PayloadError if a read error occurred. 169 """ 170 if not self.header: 171 raise PayloadError('payload header not present') 172 173 return common.Read( 174 self.payload_file, self.header.metadata_signature_len, 175 offset=self.payload_file_offset + self.header.size + 176 self.header.manifest_len) 177 178 def ReadDataBlob(self, offset, length): 179 """Reads and returns a single data blob from the update payload. 180 181 Args: 182 offset: offset to the beginning of the blob from the end of the manifest 183 length: the blob's length 184 185 Returns: 186 A string containing the raw blob data. 187 188 Raises: 189 PayloadError if a read error occurred. 190 """ 191 return common.Read(self.payload_file, length, 192 offset=self.payload_file_offset + self.data_offset + 193 offset) 194 195 def Init(self): 196 """Initializes the payload object. 197 198 This is a prerequisite for any other public API call. 199 200 Raises: 201 PayloadError if object already initialized or fails to initialize 202 correctly. 203 """ 204 if self.is_init: 205 raise PayloadError('payload object already initialized') 206 207 self.manifest_hasher = hashlib.sha256() 208 209 # Read the file header. 210 self.payload_file.seek(self.payload_file_offset) 211 self.header = self._ReadHeader() 212 213 # Read the manifest. 214 manifest_raw = self._ReadManifest() 215 self.manifest = update_metadata_pb2.DeltaArchiveManifest() 216 self.manifest.ParseFromString(manifest_raw) 217 218 # Read the metadata signature (if any). 219 metadata_signature_raw = self._ReadMetadataSignature() 220 if metadata_signature_raw: 221 self.metadata_signature = update_metadata_pb2.Signatures() 222 self.metadata_signature.ParseFromString(metadata_signature_raw) 223 224 self.metadata_size = self.header.size + self.header.manifest_len 225 self.data_offset = self.metadata_size + self.header.metadata_signature_len 226 227 self.is_init = True 228 229 def Describe(self): 230 """Emits the payload embedded description data to standard output.""" 231 def _DescribeImageInfo(description, image_info): 232 """Display info about the image.""" 233 def _DisplayIndentedValue(name, value): 234 print(' {:<14} {}'.format(name+':', value)) 235 236 print('%s:' % description) 237 _DisplayIndentedValue('Channel', image_info.channel) 238 _DisplayIndentedValue('Board', image_info.board) 239 _DisplayIndentedValue('Version', image_info.version) 240 _DisplayIndentedValue('Key', image_info.key) 241 242 if image_info.build_channel != image_info.channel: 243 _DisplayIndentedValue('Build channel', image_info.build_channel) 244 245 if image_info.build_version != image_info.version: 246 _DisplayIndentedValue('Build version', image_info.build_version) 247 248 if self.manifest.HasField('old_image_info'): 249 _DescribeImageInfo('Old Image', self.manifest.old_image_info) 250 251 if self.manifest.HasField('new_image_info'): 252 _DescribeImageInfo('New Image', self.manifest.new_image_info) 253 254 def _AssertInit(self): 255 """Raises an exception if the object was not initialized.""" 256 if not self.is_init: 257 raise PayloadError('payload object not initialized') 258 259 def ResetFile(self): 260 """Resets the offset of the payload file to right past the manifest.""" 261 self.payload_file.seek(self.payload_file_offset + self.data_offset) 262 263 def IsDelta(self): 264 """Returns True iff the payload appears to be a delta.""" 265 self._AssertInit() 266 return (self.manifest.HasField('old_kernel_info') or 267 self.manifest.HasField('old_rootfs_info') or 268 any(partition.HasField('old_partition_info') 269 for partition in self.manifest.partitions)) 270 271 def IsFull(self): 272 """Returns True iff the payload appears to be a full.""" 273 return not self.IsDelta() 274 275 def Check(self, pubkey_file_name=None, metadata_sig_file=None, 276 metadata_size=0, report_out_file=None, assert_type=None, 277 block_size=0, part_sizes=None, allow_unhashed=False, 278 disabled_tests=()): 279 """Checks the payload integrity. 280 281 Args: 282 pubkey_file_name: public key used for signature verification 283 metadata_sig_file: metadata signature, if verification is desired 284 metadata_size: metadata size, if verification is desired 285 report_out_file: file object to dump the report to 286 assert_type: assert that payload is either 'full' or 'delta' 287 block_size: expected filesystem / payload block size 288 part_sizes: map of partition label to (physical) size in bytes 289 allow_unhashed: allow unhashed operation blobs 290 disabled_tests: list of tests to disable 291 292 Raises: 293 PayloadError if payload verification failed. 294 """ 295 self._AssertInit() 296 297 # Create a short-lived payload checker object and run it. 298 helper = checker.PayloadChecker( 299 self, assert_type=assert_type, block_size=block_size, 300 allow_unhashed=allow_unhashed, disabled_tests=disabled_tests) 301 helper.Run(pubkey_file_name=pubkey_file_name, 302 metadata_sig_file=metadata_sig_file, 303 metadata_size=metadata_size, 304 part_sizes=part_sizes, 305 report_out_file=report_out_file) 306 307 def Apply(self, new_parts, old_parts=None, bsdiff_in_place=True, 308 bspatch_path=None, puffpatch_path=None, 309 truncate_to_expected_size=True): 310 """Applies the update payload. 311 312 Args: 313 new_parts: map of partition name to dest partition file 314 old_parts: map of partition name to partition file (optional) 315 bsdiff_in_place: whether to perform BSDIFF operations in-place (optional) 316 bspatch_path: path to the bspatch binary (optional) 317 puffpatch_path: path to the puffpatch binary (optional) 318 truncate_to_expected_size: whether to truncate the resulting partitions 319 to their expected sizes, as specified in the 320 payload (optional) 321 322 Raises: 323 PayloadError if payload application failed. 324 """ 325 self._AssertInit() 326 327 # Create a short-lived payload applier object and run it. 328 helper = applier.PayloadApplier( 329 self, bsdiff_in_place=bsdiff_in_place, bspatch_path=bspatch_path, 330 puffpatch_path=puffpatch_path, 331 truncate_to_expected_size=truncate_to_expected_size) 332 helper.Run(new_parts, old_parts=old_parts) 333