• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(
131          payload_fp.fileno(), 0, access=mmap.ACCESS_READ)
132      self.payload_file = io.BytesIO(payload_bytes)
133    else:
134      self.payload_file = payload_file
135    self.payload_file_offset = payload_file_offset
136    self.manifest_hasher = None
137    self.is_init = False
138    self.header = None
139    self.manifest = None
140    self.data_offset = None
141    self.metadata_signature = None
142    self.payload_signature = None
143    self.metadata_size = None
144
145  @property
146  def is_incremental(self):
147    return any([part.HasField("old_partition_info") for part in self.manifest.partitions])
148
149  @property
150  def is_partial(self):
151    return self.manifest.partial_update
152
153  def _ReadHeader(self):
154    """Reads and returns the payload header.
155
156    Returns:
157      A payload header object.
158
159    Raises:
160      PayloadError if a read error occurred.
161    """
162    header = self._PayloadHeader()
163    header.ReadFromPayload(self.payload_file, self.manifest_hasher)
164    return header
165
166  def _ReadManifest(self):
167    """Reads and returns the payload manifest.
168
169    Returns:
170      A string containing the payload manifest in binary form.
171
172    Raises:
173      PayloadError if a read error occurred.
174    """
175    if not self.header:
176      raise PayloadError('payload header not present')
177
178    return common.Read(self.payload_file, self.header.manifest_len,
179                       hasher=self.manifest_hasher)
180
181  def _ReadMetadataSignature(self):
182    """Reads and returns the metadata signatures.
183
184    Returns:
185      A string containing the metadata signatures protobuf in binary form or
186      an empty string if no metadata signature found in the payload.
187
188    Raises:
189      PayloadError if a read error occurred.
190    """
191    if not self.header:
192      raise PayloadError('payload header not present')
193
194    return common.Read(
195        self.payload_file, self.header.metadata_signature_len,
196        offset=self.payload_file_offset + self.header.size +
197        self.header.manifest_len)
198
199  def ReadDataBlob(self, offset, length):
200    """Reads and returns a single data blob from the update payload.
201
202    Args:
203      offset: offset to the beginning of the blob from the end of the manifest
204      length: the blob's length
205
206    Returns:
207      A string containing the raw blob data.
208
209    Raises:
210      PayloadError if a read error occurred.
211    """
212    return common.Read(self.payload_file, length,
213                       offset=self.payload_file_offset + self.data_offset +
214                       offset)
215
216  def Init(self):
217    """Initializes the payload object.
218
219    This is a prerequisite for any other public API call.
220
221    Raises:
222      PayloadError if object already initialized or fails to initialize
223      correctly.
224    """
225    if self.is_init:
226      raise PayloadError('payload object already initialized')
227
228    self.manifest_hasher = hashlib.sha256()
229
230    # Read the file header.
231    self.payload_file.seek(self.payload_file_offset)
232    self.header = self._ReadHeader()
233
234    # Read the manifest.
235    manifest_raw = self._ReadManifest()
236    self.manifest = update_metadata_pb2.DeltaArchiveManifest()
237    self.manifest.ParseFromString(manifest_raw)
238
239    # Read the metadata signature (if any).
240    metadata_signature_raw = self._ReadMetadataSignature()
241    if metadata_signature_raw:
242      self.metadata_signature = update_metadata_pb2.Signatures()
243      self.metadata_signature.ParseFromString(metadata_signature_raw)
244
245    self.metadata_size = self.header.size + self.header.manifest_len
246    self.data_offset = self.metadata_size + self.header.metadata_signature_len
247
248    if self.manifest.signatures_offset and self.manifest.signatures_size:
249      payload_signature_blob = self.ReadDataBlob(
250          self.manifest.signatures_offset, self.manifest.signatures_size)
251      payload_signature = update_metadata_pb2.Signatures()
252      payload_signature.ParseFromString(payload_signature_blob)
253      self.payload_signature = payload_signature
254
255    self.is_init = True
256
257  def _AssertInit(self):
258    """Raises an exception if the object was not initialized."""
259    if not self.is_init:
260      raise PayloadError('payload object not initialized')
261
262  def ResetFile(self):
263    """Resets the offset of the payload file to right past the manifest."""
264    self.payload_file.seek(self.payload_file_offset + self.data_offset)
265
266  def IsDelta(self):
267    """Returns True iff the payload appears to be a delta."""
268    self._AssertInit()
269    return (any(partition.HasField('old_partition_info')
270                for partition in self.manifest.partitions))
271
272  def IsFull(self):
273    """Returns True iff the payload appears to be a full."""
274    return not self.IsDelta()
275
276  def Check(self, pubkey_file_name=None, metadata_sig_file=None,
277            metadata_size=0, report_out_file=None, assert_type=None,
278            block_size=0, part_sizes=None, allow_unhashed=False,
279            disabled_tests=()):
280    """Checks the payload integrity.
281
282    Args:
283      pubkey_file_name: public key used for signature verification
284      metadata_sig_file: metadata signature, if verification is desired
285      metadata_size: metadata size, if verification is desired
286      report_out_file: file object to dump the report to
287      assert_type: assert that payload is either 'full' or 'delta'
288      block_size: expected filesystem / payload block size
289      part_sizes: map of partition label to (physical) size in bytes
290      allow_unhashed: allow unhashed operation blobs
291      disabled_tests: list of tests to disable
292
293    Raises:
294      PayloadError if payload verification failed.
295    """
296    self._AssertInit()
297
298    # Create a short-lived payload checker object and run it.
299    helper = checker.PayloadChecker(
300        self, assert_type=assert_type, block_size=block_size,
301        allow_unhashed=allow_unhashed, disabled_tests=disabled_tests)
302    helper.Run(pubkey_file_name=pubkey_file_name,
303               metadata_sig_file=metadata_sig_file,
304               metadata_size=metadata_size,
305               part_sizes=part_sizes,
306               report_out_file=report_out_file)
307
308  def Apply(self, new_parts, old_parts=None, bsdiff_in_place=True,
309            bspatch_path=None, puffpatch_path=None,
310            truncate_to_expected_size=True):
311    """Applies the update payload.
312
313    Args:
314      new_parts: map of partition name to dest partition file
315      old_parts: map of partition name to partition file (optional)
316      bsdiff_in_place: whether to perform BSDIFF operations in-place (optional)
317      bspatch_path: path to the bspatch binary (optional)
318      puffpatch_path: path to the puffpatch binary (optional)
319      truncate_to_expected_size: whether to truncate the resulting partitions
320                                 to their expected sizes, as specified in the
321                                 payload (optional)
322
323    Raises:
324      PayloadError if payload application failed.
325    """
326    self._AssertInit()
327
328    # Create a short-lived payload applier object and run it.
329    helper = applier.PayloadApplier(
330        self, bsdiff_in_place=bsdiff_in_place, bspatch_path=bspatch_path,
331        puffpatch_path=puffpatch_path,
332        truncate_to_expected_size=truncate_to_expected_size)
333    helper.Run(new_parts, old_parts=old_parts)
334