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 absolute_import 20from __future__ import print_function 21 22import hashlib 23import struct 24 25from update_payload import applier 26from update_payload import checker 27from update_payload import common 28from update_payload import update_metadata_pb2 29from update_payload.error import PayloadError 30 31 32# 33# Helper functions. 34# 35def _ReadInt(file_obj, size, is_unsigned, hasher=None): 36 """Reads a binary-encoded integer from a file. 37 38 It will do the correct conversion based on the reported size and whether or 39 not a signed number is expected. Assumes a network (big-endian) byte 40 ordering. 41 42 Args: 43 file_obj: a file object 44 size: the integer size in bytes (2, 4 or 8) 45 is_unsigned: whether it is signed or not 46 hasher: an optional hasher to pass the value through 47 48 Returns: 49 An "unpacked" (Python) integer value. 50 51 Raises: 52 PayloadError if an read error occurred. 53 """ 54 return struct.unpack(common.IntPackingFmtStr(size, is_unsigned), 55 common.Read(file_obj, size, hasher=hasher))[0] 56 57 58# 59# Update payload. 60# 61class Payload(object): 62 """Chrome OS update payload processor.""" 63 64 class _PayloadHeader(object): 65 """Update payload header struct.""" 66 67 # Header constants; sizes are in bytes. 68 _MAGIC = b'CrAU' 69 _VERSION_SIZE = 8 70 _MANIFEST_LEN_SIZE = 8 71 _METADATA_SIGNATURE_LEN_SIZE = 4 72 73 def __init__(self): 74 self.version = None 75 self.manifest_len = None 76 self.metadata_signature_len = None 77 self.size = None 78 79 def ReadFromPayload(self, payload_file, hasher=None): 80 """Reads the payload header from a file. 81 82 Reads the payload header from the |payload_file| and updates the |hasher| 83 if one is passed. The parsed header is stored in the _PayloadHeader 84 instance attributes. 85 86 Args: 87 payload_file: a file object 88 hasher: an optional hasher to pass the value through 89 90 Returns: 91 None. 92 93 Raises: 94 PayloadError if a read error occurred or the header is invalid. 95 """ 96 # Verify magic 97 magic = common.Read(payload_file, len(self._MAGIC), hasher=hasher) 98 if magic != self._MAGIC: 99 raise PayloadError('invalid payload magic: %s' % magic) 100 101 self.version = _ReadInt(payload_file, self._VERSION_SIZE, True, 102 hasher=hasher) 103 self.manifest_len = _ReadInt(payload_file, self._MANIFEST_LEN_SIZE, True, 104 hasher=hasher) 105 self.size = (len(self._MAGIC) + self._VERSION_SIZE + 106 self._MANIFEST_LEN_SIZE) 107 self.metadata_signature_len = 0 108 109 if self.version == common.BRILLO_MAJOR_PAYLOAD_VERSION: 110 self.size += self._METADATA_SIGNATURE_LEN_SIZE 111 self.metadata_signature_len = _ReadInt( 112 payload_file, self._METADATA_SIGNATURE_LEN_SIZE, True, 113 hasher=hasher) 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 (any(partition.HasField('old_partition_info') 267 for partition in self.manifest.partitions)) 268 269 def IsFull(self): 270 """Returns True iff the payload appears to be a full.""" 271 return not self.IsDelta() 272 273 def Check(self, pubkey_file_name=None, metadata_sig_file=None, 274 metadata_size=0, report_out_file=None, assert_type=None, 275 block_size=0, part_sizes=None, allow_unhashed=False, 276 disabled_tests=()): 277 """Checks the payload integrity. 278 279 Args: 280 pubkey_file_name: public key used for signature verification 281 metadata_sig_file: metadata signature, if verification is desired 282 metadata_size: metadata size, if verification is desired 283 report_out_file: file object to dump the report to 284 assert_type: assert that payload is either 'full' or 'delta' 285 block_size: expected filesystem / payload block size 286 part_sizes: map of partition label to (physical) size in bytes 287 allow_unhashed: allow unhashed operation blobs 288 disabled_tests: list of tests to disable 289 290 Raises: 291 PayloadError if payload verification failed. 292 """ 293 self._AssertInit() 294 295 # Create a short-lived payload checker object and run it. 296 helper = checker.PayloadChecker( 297 self, assert_type=assert_type, block_size=block_size, 298 allow_unhashed=allow_unhashed, disabled_tests=disabled_tests) 299 helper.Run(pubkey_file_name=pubkey_file_name, 300 metadata_sig_file=metadata_sig_file, 301 metadata_size=metadata_size, 302 part_sizes=part_sizes, 303 report_out_file=report_out_file) 304 305 def Apply(self, new_parts, old_parts=None, bsdiff_in_place=True, 306 bspatch_path=None, puffpatch_path=None, 307 truncate_to_expected_size=True): 308 """Applies the update payload. 309 310 Args: 311 new_parts: map of partition name to dest partition file 312 old_parts: map of partition name to partition file (optional) 313 bsdiff_in_place: whether to perform BSDIFF operations in-place (optional) 314 bspatch_path: path to the bspatch binary (optional) 315 puffpatch_path: path to the puffpatch binary (optional) 316 truncate_to_expected_size: whether to truncate the resulting partitions 317 to their expected sizes, as specified in the 318 payload (optional) 319 320 Raises: 321 PayloadError if payload application failed. 322 """ 323 self._AssertInit() 324 325 # Create a short-lived payload applier object and run it. 326 helper = applier.PayloadApplier( 327 self, bsdiff_in_place=bsdiff_in_place, bspatch_path=bspatch_path, 328 puffpatch_path=puffpatch_path, 329 truncate_to_expected_size=truncate_to_expected_size) 330 helper.Run(new_parts, old_parts=old_parts) 331