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