• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2
3# Copyright 2019, The Android Open Source Project
4#
5# Permission is hereby granted, free of charge, to any person
6# obtaining a copy of this software and associated documentation
7# files (the "Software"), to deal in the Software without
8# restriction, including without limitation the rights to use, copy,
9# modify, merge, publish, distribute, sublicense, and/or sell copies
10# of the Software, and to permit persons to whom the Software is
11# furnished to do so, subject to the following conditions:
12#
13# The above copyright notice and this permission notice shall be
14# included in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
20# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23# SOFTWARE.
24#
25
26"""Tool for verifying VBMeta & calculate VBMeta Digests of Pixel factory images.
27
28If given an HTTPS URL it will download the file first before processing.
29$ pixel_factory_image_verify.py https://dl.google.com/dl/android/aosp/image.zip
30
31Otherwise, the argument is considered to be a local file.
32$ pixel_factory_image_verify.py image.zip
33
34The list of canonical Pixel factory images can be found here:
35https://developers.google.com/android/images
36
37Supported are all factory images of Pixel 3 and later devices.
38
39In order for the tool to run correct the following utilities need to be
40pre-installed: wget, unzip.
41
42The tool also runs outside of the repository location as long as the working
43directory is writable.
44"""
45
46from __future__ import print_function
47
48import os
49import shutil
50import subprocess
51import sys
52import tempfile
53import distutils.spawn
54
55
56class PixelFactoryImageVerifier(object):
57  """Object for the pixel_factory_image_verify command line tool."""
58
59  def __init__(self):
60    self.working_dir = os.getcwd()
61    self.script_path = os.path.realpath(__file__)
62    self.script_dir = os.path.split(self.script_path)[0]
63    self.avbtool_path = os.path.abspath(os.path.join(self.script_path,
64                                                     '../../../avbtool'))
65
66  def run(self, argv):
67    """Command line processor.
68
69    Args:
70       argv: The command line parameter list.
71    """
72    # Checks for command line parameters and show help if non given.
73    if len(argv) != 2:
74      print('No command line parameter given. At least a filename or URL for a '
75            'Pixel 3 or later factory image needs to be specified.')
76      sys.exit(1)
77
78    # Checks if necessary commands are available.
79    for cmd in ['grep', 'unzip', 'wget']:
80      if not distutils.spawn.find_executable(cmd):
81        print('Necessary command line tool needs to be installed first: %s'
82              % cmd)
83        sys.exit(1)
84
85    # Downloads factory image if URL is specified; otherwise treat it as file.
86    if argv[1].lower().startswith('https://'):
87      factory_image_zip = self._download_factory_image(argv[1])
88      if not factory_image_zip:
89        sys.exit(1)
90    else:
91      factory_image_zip = os.path.abspath(argv[1])
92
93    # Unpacks the factory image into partition images.
94    partition_image_dir = self._unpack_factory_image(factory_image_zip)
95    if not partition_image_dir:
96      sys.exit(1)
97
98    # Validates the VBMeta of the factory image.
99    verified = self._verify_vbmeta_partitions(partition_image_dir)
100    if not verified:
101      sys.exit(1)
102
103    fingerprint = self._extract_build_fingerprint(partition_image_dir)
104    if not fingerprint:
105      sys.exit(1)
106
107    # Calculates the VBMeta Digest for the factory image.
108    vbmeta_digest = self._calculate_vbmeta_digest(partition_image_dir)
109    if not vbmeta_digest:
110      sys.exit(1)
111
112    print('The build fingerprint for factory image is: %s' % fingerprint)
113    print('The VBMeta Digest for factory image is: %s' % vbmeta_digest)
114    sys.exit(0)
115
116  def _download_factory_image(self, url):
117    """Downloads the factory image to the working directory.
118
119    Args:
120      url: The download URL for the factory image.
121
122    Returns:
123      The absolute path to the factory image or None if it failed.
124    """
125    # Creates temporary download folder.
126    download_path = tempfile.mkdtemp(dir=self.working_dir)
127
128    # Downloads the factory image to the temporary folder.
129    download_filename = self._download_file(download_path, url)
130    if not download_filename:
131      return None
132
133    # Moves the downloaded file into the working directory.
134    download_file = os.path.join(download_path, download_filename)
135    target_file = os.path.join(self.working_dir, download_filename)
136    if os.path.exists(target_file):
137      try:
138        os.remove(target_file)
139      except OSError as e:
140        print('File %s already exists and cannot be deleted.' % download_file)
141        return None
142    try:
143      shutil.move(download_file, self.working_dir)
144    except shutil.Error as e:
145      print('File %s cannot be moved to %s: %s' % (download_file,
146                                                   target_file, e))
147      return None
148
149    # Removes temporary download folder.
150    try:
151      shutil.rmtree(download_path)
152    except shutil.Error as e:
153      print('Temporary download folder %s could not be removed.'
154            % download_path)
155    return os.path.join(self.working_dir, download_filename)
156
157  def _download_file(self, download_dir, url):
158    """Downloads a file from the Internet.
159
160    Args:
161      download_dir: The folder the file should be downloaded to.
162      url: The download URL for the file.
163
164    Returns:
165      The name of the downloaded file as it apears on disk; otherwise None
166      if download failed.
167    """
168    print('Fetching file from: %s' % url)
169    os.chdir(download_dir)
170    args = ['wget', url]
171    result, _ = self._run_command(args,
172                                  'Successfully downloaded file.',
173                                  'File download failed.')
174    os.chdir(self.working_dir)
175    if not result:
176      return None
177
178    # Figure out the file name of what was downloaded: It will be the only file
179    # in the download folder.
180    files = os.listdir(download_dir)
181    if files and len(files) == 1:
182      return files[0]
183    else:
184      return None
185
186  def _unpack_factory_image(self, factory_image_file):
187    """Unpacks the factory image zip file.
188
189    Args:
190      factory_image_file: path and file name to the image file.
191
192    Returns:
193      The path to the folder which contains the unpacked factory image files or
194      None if it failed.
195    """
196    unpack_dir = tempfile.mkdtemp(dir=self.working_dir)
197    args = ['unzip', factory_image_file, '-d', unpack_dir]
198    result, _ = self._run_command(args,
199                                  'Successfully unpacked factory image.',
200                                  'Failed to unpack factory image.')
201    if not result:
202      return None
203
204    # Locate the directory which contains the image files.
205    files = os.listdir(unpack_dir)
206    image_name = None
207    for f in files:
208      path = os.path.join(self.working_dir, unpack_dir, f)
209      if os.path.isdir(path):
210        image_name = f
211        break
212    if not image_name:
213      print('No image found: %s' % image_name)
214      return None
215
216    # Move image file directory to the working directory
217    image_dir = os.path.join(unpack_dir, image_name)
218    target_dir = os.path.join(self.working_dir, image_name)
219    if os.path.exists(target_dir):
220      try:
221        shutil.rmtree(target_dir)
222      except shutil.Error as e:
223        print('Directory %s already exists and cannot be deleted.' % target_dir)
224        return None
225
226    try:
227      shutil.move(image_dir, self.working_dir)
228    except shutil.Error as e:
229      print('Directory %s could not be moved to %s: %s' % (image_dir,
230                                                           self.working_dir, e))
231      return None
232
233    # Removes tmp unpack directory.
234    try:
235      shutil.rmtree(unpack_dir)
236    except shutil.Error as e:
237      print('Temporary download folder %s could not be removed.'
238            % unpack_dir)
239
240    # Unzip the secondary zip file which contain the individual images.
241    image_filename = 'image-%s' % image_name
242    image_folder = os.path.join(self.working_dir, image_name)
243    os.chdir(image_folder)
244
245    args = ['unzip', image_filename]
246    result, _ = self._run_command(
247        args,
248        'Successfully unpacked factory image partitions.',
249        'Failed to unpack factory image partitions.')
250    if not result:
251      return None
252    return image_folder
253
254  def _verify_vbmeta_partitions(self, image_dir):
255    """Verifies all partitions protected by VBMeta using avbtool verify_image.
256
257    Args:
258      image_dir: The folder containing the unpacked factory image partitions,
259      which contains a vbmeta.img patition.
260
261    Returns:
262      True if the VBMeta protected parititions verify.
263    """
264    os.chdir(image_dir)
265    args = [self.avbtool_path,
266            'verify_image',
267            '--image', 'vbmeta.img',
268            '--follow_chain_partitions']
269    result, _ = self._run_command(args,
270                                  'Successfully verified VBmeta.',
271                                  'Verification of VBmeta failed.')
272    os.chdir(self.working_dir)
273    return result
274
275  def _extract_build_fingerprint(self, image_dir):
276    """Extracts the build fingerprint from the system.img.
277    Args:
278      image_dir: The folder containing the unpacked factory image partitions,
279	    which contains a vbmeta.img patition.
280
281    Returns:
282      The build fingerprint string, e.g.
283      google/blueline/blueline:9/PQ2A.190305.002/5240760:user/release-keys
284    """
285    os.chdir(image_dir)
286    args = ['grep',
287            '-a',
288            'ro\.build\.fingerprint=google/.*/release-keys',
289            'system.img']
290
291    result, output = self._run_command(
292        args,
293        'Successfully extracted build fingerpint.',
294        'Build fingerprint extraction failed.')
295    os.chdir(self.working_dir)
296    if result:
297      _, fingerprint = output.split('=', 1)
298      return fingerprint.rstrip()
299    else:
300      return None
301
302  def _calculate_vbmeta_digest(self, image_dir):
303    """Calculates the VBMeta Digest for given parititions using avbtool.
304
305    Args:
306      image_dir: The folder containing the unpacked factory image partitions,
307        which contains a vbmeta.img partition.
308
309    Returns:
310      Hex string with the VBmeta Digest value or None if it failed.
311    """
312    os.chdir(image_dir)
313    args = [self.avbtool_path,
314            'calculate_vbmeta_digest',
315            '--image', 'vbmeta.img']
316    result, output = self._run_command(args,
317                                       'Successfully calculated VBMeta Digest.',
318                                       'Failed to calculate VBmeta Digest.')
319    os.chdir(self.working_dir)
320    if result:
321      return output
322    else:
323      return None
324
325  def _run_command(self, args, success_msg, fail_msg):
326    """Runs command line tools."""
327    p = subprocess.Popen(args, stdin=subprocess.PIPE,
328                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
329    pout, _ = p.communicate()
330    if p.wait() == 0:
331      print(success_msg)
332      return True, pout
333    else:
334      print(fail_msg)
335      return False, pout
336
337
338if __name__ == '__main__':
339  tool = PixelFactoryImageVerifier()
340  tool.run(sys.argv)
341
342