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 io 24import mmap 25import struct 26import zipfile 27 28from update_payload import applier 29from update_payload import checker 30from update_payload import common 31from update_payload import update_metadata_pb2 32from update_payload.error import PayloadError 33 34 35# 36# Helper functions. 37# 38def _ReadInt(file_obj, size, is_unsigned, hasher=None): 39 """Reads a binary-encoded integer from a file. 40 41 It will do the correct conversion based on the reported size and whether or 42 not a signed number is expected. Assumes a network (big-endian) byte 43 ordering. 44 45 Args: 46 file_obj: a file object 47 size: the integer size in bytes (2, 4 or 8) 48 is_unsigned: whether it is signed or not 49 hasher: an optional hasher to pass the value through 50 51 Returns: 52 An "unpacked" (Python) integer value. 53 54 Raises: 55 PayloadError if an read error occurred. 56 """ 57 return struct.unpack(common.IntPackingFmtStr(size, is_unsigned), 58 common.Read(file_obj, size, hasher=hasher))[0] 59 60 61# 62# Update payload. 63# 64class Payload(object): 65 """Chrome OS update payload processor.""" 66 67 class _PayloadHeader(object): 68 """Update payload header struct.""" 69 70 # Header constants; sizes are in bytes. 71 _MAGIC = b'CrAU' 72 _VERSION_SIZE = 8 73 _MANIFEST_LEN_SIZE = 8 74 _METADATA_SIGNATURE_LEN_SIZE = 4 75 76 def __init__(self): 77 self.version = None 78 self.manifest_len = None 79 self.metadata_signature_len = None 80 self.size = None 81 82 def ReadFromPayload(self, payload_file, hasher=None): 83 """Reads the payload header from a file. 84 85 Reads the payload header from the |payload_file| and updates the |hasher| 86 if one is passed. The parsed header is stored in the _PayloadHeader 87 instance attributes. 88 89 Args: 90 payload_file: a file object 91 hasher: an optional hasher to pass the value through 92 93 Returns: 94 None. 95 96 Raises: 97 PayloadError if a read error occurred or the header is invalid. 98 """ 99 # Verify magic 100 magic = common.Read(payload_file, len(self._MAGIC), hasher=hasher) 101 if magic != self._MAGIC: 102 raise PayloadError('invalid payload magic: %s' % magic) 103 104 self.version = _ReadInt(payload_file, self._VERSION_SIZE, True, 105 hasher=hasher) 106 self.manifest_len = _ReadInt(payload_file, self._MANIFEST_LEN_SIZE, True, 107 hasher=hasher) 108 self.size = (len(self._MAGIC) + self._VERSION_SIZE + 109 self._MANIFEST_LEN_SIZE) 110 self.metadata_signature_len = 0 111 112 if self.version == common.BRILLO_MAJOR_PAYLOAD_VERSION: 113 self.size += self._METADATA_SIGNATURE_LEN_SIZE 114 self.metadata_signature_len = _ReadInt( 115 payload_file, self._METADATA_SIGNATURE_LEN_SIZE, True, 116 hasher=hasher) 117 118 def __init__(self, payload_file, payload_file_offset=0): 119 """Initialize the payload object. 120 121 Args: 122 payload_file: update payload file object open for reading 123 payload_file_offset: the offset of the actual payload 124 """ 125 if zipfile.is_zipfile(payload_file): 126 with zipfile.ZipFile(payload_file) as zfp: 127 self.payload_file = zfp.open("payload.bin", "r") 128 elif isinstance(payload_file, str): 129 payload_fp = open(payload_file, "rb") 130 payload_bytes = mmap.mmap(payload_fp.fileno(), 0, access=mmap.ACCESS_READ) 131 self.payload_file = io.BytesIO(payload_bytes) 132 else: 133 self.payload_file = payload_file 134 self.payload_file_offset = payload_file_offset 135 self.manifest_hasher = None 136 self.is_init = False 137 self.header = None 138 self.manifest = None 139 self.data_offset = None 140 self.metadata_signature = None 141 self.metadata_size = None 142 143 def _ReadHeader(self): 144 """Reads and returns the payload header. 145 146 Returns: 147 A payload header object. 148 149 Raises: 150 PayloadError if a read error occurred. 151 """ 152 header = self._PayloadHeader() 153 header.ReadFromPayload(self.payload_file, self.manifest_hasher) 154 return header 155 156 def _ReadManifest(self): 157 """Reads and returns the payload manifest. 158 159 Returns: 160 A string containing the payload manifest in binary form. 161 162 Raises: 163 PayloadError if a read error occurred. 164 """ 165 if not self.header: 166 raise PayloadError('payload header not present') 167 168 return common.Read(self.payload_file, self.header.manifest_len, 169 hasher=self.manifest_hasher) 170 171 def _ReadMetadataSignature(self): 172 """Reads and returns the metadata signatures. 173 174 Returns: 175 A string containing the metadata signatures protobuf in binary form or 176 an empty string if no metadata signature found in the payload. 177 178 Raises: 179 PayloadError if a read error occurred. 180 """ 181 if not self.header: 182 raise PayloadError('payload header not present') 183 184 return common.Read( 185 self.payload_file, self.header.metadata_signature_len, 186 offset=self.payload_file_offset + self.header.size + 187 self.header.manifest_len) 188 189 def ReadDataBlob(self, offset, length): 190 """Reads and returns a single data blob from the update payload. 191 192 Args: 193 offset: offset to the beginning of the blob from the end of the manifest 194 length: the blob's length 195 196 Returns: 197 A string containing the raw blob data. 198 199 Raises: 200 PayloadError if a read error occurred. 201 """ 202 return common.Read(self.payload_file, length, 203 offset=self.payload_file_offset + self.data_offset + 204 offset) 205 206 def Init(self): 207 """Initializes the payload object. 208 209 This is a prerequisite for any other public API call. 210 211 Raises: 212 PayloadError if object already initialized or fails to initialize 213 correctly. 214 """ 215 if self.is_init: 216 raise PayloadError('payload object already initialized') 217 218 self.manifest_hasher = hashlib.sha256() 219 220 # Read the file header. 221 self.payload_file.seek(self.payload_file_offset) 222 self.header = self._ReadHeader() 223 224 # Read the manifest. 225 manifest_raw = self._ReadManifest() 226 self.manifest = update_metadata_pb2.DeltaArchiveManifest() 227 self.manifest.ParseFromString(manifest_raw) 228 229 # Read the metadata signature (if any). 230 metadata_signature_raw = self._ReadMetadataSignature() 231 if metadata_signature_raw: 232 self.metadata_signature = update_metadata_pb2.Signatures() 233 self.metadata_signature.ParseFromString(metadata_signature_raw) 234 235 self.metadata_size = self.header.size + self.header.manifest_len 236 self.data_offset = self.metadata_size + self.header.metadata_signature_len 237 238 self.is_init = True 239 240 def _AssertInit(self): 241 """Raises an exception if the object was not initialized.""" 242 if not self.is_init: 243 raise PayloadError('payload object not initialized') 244 245 def ResetFile(self): 246 """Resets the offset of the payload file to right past the manifest.""" 247 self.payload_file.seek(self.payload_file_offset + self.data_offset) 248 249 def IsDelta(self): 250 """Returns True iff the payload appears to be a delta.""" 251 self._AssertInit() 252 return (any(partition.HasField('old_partition_info') 253 for partition in self.manifest.partitions)) 254 255 def IsFull(self): 256 """Returns True iff the payload appears to be a full.""" 257 return not self.IsDelta() 258 259 def Check(self, pubkey_file_name=None, metadata_sig_file=None, 260 metadata_size=0, report_out_file=None, assert_type=None, 261 block_size=0, part_sizes=None, allow_unhashed=False, 262 disabled_tests=()): 263 """Checks the payload integrity. 264 265 Args: 266 pubkey_file_name: public key used for signature verification 267 metadata_sig_file: metadata signature, if verification is desired 268 metadata_size: metadata size, if verification is desired 269 report_out_file: file object to dump the report to 270 assert_type: assert that payload is either 'full' or 'delta' 271 block_size: expected filesystem / payload block size 272 part_sizes: map of partition label to (physical) size in bytes 273 allow_unhashed: allow unhashed operation blobs 274 disabled_tests: list of tests to disable 275 276 Raises: 277 PayloadError if payload verification failed. 278 """ 279 self._AssertInit() 280 281 # Create a short-lived payload checker object and run it. 282 helper = checker.PayloadChecker( 283 self, assert_type=assert_type, block_size=block_size, 284 allow_unhashed=allow_unhashed, disabled_tests=disabled_tests) 285 helper.Run(pubkey_file_name=pubkey_file_name, 286 metadata_sig_file=metadata_sig_file, 287 metadata_size=metadata_size, 288 part_sizes=part_sizes, 289 report_out_file=report_out_file) 290 291 def Apply(self, new_parts, old_parts=None, bsdiff_in_place=True, 292 bspatch_path=None, puffpatch_path=None, 293 truncate_to_expected_size=True): 294 """Applies the update payload. 295 296 Args: 297 new_parts: map of partition name to dest partition file 298 old_parts: map of partition name to partition file (optional) 299 bsdiff_in_place: whether to perform BSDIFF operations in-place (optional) 300 bspatch_path: path to the bspatch binary (optional) 301 puffpatch_path: path to the puffpatch binary (optional) 302 truncate_to_expected_size: whether to truncate the resulting partitions 303 to their expected sizes, as specified in the 304 payload (optional) 305 306 Raises: 307 PayloadError if payload application failed. 308 """ 309 self._AssertInit() 310 311 # Create a short-lived payload applier object and run it. 312 helper = applier.PayloadApplier( 313 self, bsdiff_in_place=bsdiff_in_place, bspatch_path=bspatch_path, 314 puffpatch_path=puffpatch_path, 315 truncate_to_expected_size=truncate_to_expected_size) 316 helper.Run(new_parts, old_parts=old_parts) 317