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