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: all factory images of Pixel 6 and later devices. 38 39In order for the tool to run correct the following utilities need to be 40pre-installed: grep, wget or curl, unzip. 41 42Additionally, make sure that the bootloader unpacker script is separately 43downloaded, made executable, and symlinked as 'fbpacktool', and made accessible 44via your shell $PATH. 45 46The tool also runs outside of the repository location as long as the working 47directory is writable. 48""" 49 50from __future__ import print_function 51 52import glob 53import os 54import shutil 55import subprocess 56import sys 57import tempfile 58import distutils.spawn 59 60 61class PixelFactoryImageVerifier(object): 62 """Object for the pixel_factory_image_verify command line tool.""" 63 64 ERR_TOOL_UNAVAIL_FMT_STR = 'Necessary command line tool needs to be installed first: %s' 65 66 def __init__(self): 67 self.working_dir = os.getcwd() 68 self.script_path = os.path.realpath(__file__) 69 self.script_dir = os.path.split(self.script_path)[0] 70 self.avbtool_path = os.path.abspath(os.path.join(self.script_path, 71 '../../../avbtool.py')) 72 self.fw_unpacker_path = distutils.spawn.find_executable('fbpacktool') 73 self.wget_path = distutils.spawn.find_executable('wget') 74 self.curl_path = distutils.spawn.find_executable('curl') 75 76 def run(self, argv): 77 """Command line processor. 78 79 Args: 80 argv: The command line parameter list. 81 """ 82 # Checks for command line parameters and show help if non given. 83 if len(argv) != 2: 84 print('No command line parameter given. At least a filename or URL for a ' 85 'Pixel 3 or later factory image needs to be specified.') 86 sys.exit(1) 87 88 # Checks if necessary commands are available. 89 for cmd in ['grep', 'unzip']: 90 if not distutils.spawn.find_executable(cmd): 91 print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % cmd) 92 sys.exit(1) 93 94 # Checks if `fbpacktool` is available. 95 if not self.fw_unpacker_path: 96 print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'fbpacktool') 97 sys.exit(1) 98 99 # Checks if either `wget` or `curl` is available. 100 if not self.wget_path and not self.curl_path: 101 print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'wget or curl') 102 sys.exit(1) 103 104 # Downloads factory image if URL is specified; otherwise treat it as file. 105 if argv[1].lower().startswith('https://'): 106 factory_image_zip = self._download_factory_image(argv[1]) 107 if not factory_image_zip: 108 sys.exit(1) 109 else: 110 factory_image_zip = os.path.abspath(argv[1]) 111 112 # Unpacks the factory image into partition images. 113 partition_image_dir = self._unpack_factory_image(factory_image_zip) 114 if not partition_image_dir: 115 sys.exit(1) 116 117 # Unpacks bootloader image into individual component images. 118 unpack_successful = self._unpack_bootloader(partition_image_dir) 119 if not unpack_successful: 120 sys.exit(1) 121 122 # Validates the VBMeta of the factory image. 123 verified = self._verify_vbmeta_partitions(partition_image_dir) 124 if not verified: 125 sys.exit(1) 126 127 fingerprint = self._extract_build_fingerprint(partition_image_dir) 128 if not fingerprint: 129 sys.exit(1) 130 131 # Calculates the VBMeta Digest for the factory image. 132 vbmeta_digest = self._calculate_vbmeta_digest(partition_image_dir) 133 if not vbmeta_digest: 134 sys.exit(1) 135 136 print('The build fingerprint for factory image is: %s' % fingerprint) 137 print('The VBMeta Digest for factory image is: %s' % vbmeta_digest) 138 139 with open('payload.txt', 'w') as f_out: 140 f_out.write(fingerprint.strip() + '\n') 141 f_out.write(vbmeta_digest.strip() + '\n') 142 print('A corresponding "payload.txt" file has been created.') 143 sys.exit(0) 144 145 def _download_factory_image(self, url): 146 """Downloads the factory image to the working directory. 147 148 Args: 149 url: The download URL for the factory image. 150 151 Returns: 152 The absolute path to the factory image or None if it failed. 153 """ 154 # Creates temporary download folder. 155 download_path = tempfile.mkdtemp(dir=self.working_dir) 156 157 # Downloads the factory image to the temporary folder. 158 download_filename = self._download_file(download_path, url) 159 if not download_filename: 160 return None 161 162 # Moves the downloaded file into the working directory. 163 download_file = os.path.join(download_path, download_filename) 164 target_file = os.path.join(self.working_dir, download_filename) 165 if os.path.exists(target_file): 166 try: 167 os.remove(target_file) 168 except OSError as e: 169 print('File %s already exists and cannot be deleted.' % download_file) 170 return None 171 try: 172 shutil.move(download_file, self.working_dir) 173 except shutil.Error as e: 174 print('File %s cannot be moved to %s: %s' % (download_file, 175 target_file, e)) 176 return None 177 178 # Removes temporary download folder. 179 try: 180 shutil.rmtree(download_path) 181 except shutil.Error as e: 182 print('Temporary download folder %s could not be removed.' 183 % download_path) 184 return os.path.join(self.working_dir, download_filename) 185 186 def _download_file(self, download_dir, url): 187 """Downloads a file from the Internet. 188 189 Args: 190 download_dir: The folder the file should be downloaded to. 191 url: The download URL for the file. 192 193 Returns: 194 The name of the downloaded file as it apears on disk; otherwise None 195 if download failed. 196 """ 197 print('Fetching file from: %s' % url) 198 os.chdir(download_dir) 199 args = [] 200 if self.wget_path: 201 args = [self.wget_path, url] 202 else: 203 args = [self.curl_path, '-O', url] 204 205 result, _ = self._run_command(args, 206 'Successfully downloaded file.', 207 'File download failed.') 208 os.chdir(self.working_dir) 209 if not result: 210 return None 211 212 # Figure out the file name of what was downloaded: It will be the only file 213 # in the download folder. 214 files = os.listdir(download_dir) 215 if files and len(files) == 1: 216 return files[0] 217 else: 218 return None 219 220 def _unpack_bootloader(self, factory_image_folder): 221 """Unpacks the bootloader to produce individual images. 222 223 Args: 224 factory_image_folder: path to the directory containing factory images. 225 226 Returns: 227 True if unpack is successful. False if otherwise. 228 """ 229 os.chdir(factory_image_folder) 230 bootloader_path = os.path.join(factory_image_folder, 'bootloader*.img') 231 glob_result = glob.glob(bootloader_path) 232 if not glob_result: 233 return False 234 235 args = [self.fw_unpacker_path, 'unpack', glob_result[0]] 236 result, _ = self._run_command(args, 237 'Successfully unpacked bootloader image.', 238 'Failed to unpack bootloader image.') 239 return result 240 241 def _unpack_factory_image(self, factory_image_file): 242 """Unpacks the factory image zip file. 243 244 Args: 245 factory_image_file: path and file name to the image file. 246 247 Returns: 248 The path to the folder which contains the unpacked factory image files or 249 None if it failed. 250 """ 251 unpack_dir = tempfile.mkdtemp(dir=self.working_dir) 252 args = ['unzip', factory_image_file, '-d', unpack_dir] 253 result, _ = self._run_command(args, 254 'Successfully unpacked factory image.', 255 'Failed to unpack factory image.') 256 if not result: 257 return None 258 259 # Locate the directory which contains the image files. 260 files = os.listdir(unpack_dir) 261 image_name = None 262 for f in files: 263 path = os.path.join(self.working_dir, unpack_dir, f) 264 if os.path.isdir(path): 265 image_name = f 266 break 267 if not image_name: 268 print('No image found: %s' % image_name) 269 return None 270 271 # Move image file directory to the working directory 272 image_dir = os.path.join(unpack_dir, image_name) 273 target_dir = os.path.join(self.working_dir, image_name) 274 if os.path.exists(target_dir): 275 try: 276 shutil.rmtree(target_dir) 277 except shutil.Error as e: 278 print('Directory %s already exists and cannot be deleted.' % target_dir) 279 return None 280 281 try: 282 shutil.move(image_dir, self.working_dir) 283 except shutil.Error as e: 284 print('Directory %s could not be moved to %s: %s' % (image_dir, 285 self.working_dir, e)) 286 return None 287 288 # Removes tmp unpack directory. 289 try: 290 shutil.rmtree(unpack_dir) 291 except shutil.Error as e: 292 print('Temporary download folder %s could not be removed.' 293 % unpack_dir) 294 295 # Unzip the secondary zip file which contain the individual images. 296 image_filename = 'image-%s' % image_name 297 image_folder = os.path.join(self.working_dir, image_name) 298 os.chdir(image_folder) 299 300 args = ['unzip', image_filename] 301 result, _ = self._run_command( 302 args, 303 'Successfully unpacked factory image partitions.', 304 'Failed to unpack factory image partitions.') 305 if not result: 306 return None 307 return image_folder 308 309 def _verify_vbmeta_partitions(self, image_dir): 310 """Verifies all partitions protected by VBMeta using avbtool verify_image. 311 312 Args: 313 image_dir: The folder containing the unpacked factory image partitions, 314 which contains a vbmeta.img patition. 315 316 Returns: 317 True if the VBMeta protected partitions verify. 318 """ 319 os.chdir(image_dir) 320 args = [self.avbtool_path, 321 'verify_image', 322 '--image', 'vbmeta.img', 323 '--follow_chain_partitions'] 324 result, _ = self._run_command(args, 325 'Successfully verified VBmeta.', 326 'Verification of VBmeta failed.') 327 os.chdir(self.working_dir) 328 return result 329 330 def _extract_build_fingerprint(self, image_dir): 331 """Extracts the build fingerprint from the system.img. 332 Args: 333 image_dir: The folder containing the unpacked factory image partitions, 334 which contains a vbmeta.img patition. 335 336 Returns: 337 The build fingerprint string, e.g. 338 google/blueline/blueline:9/PQ2A.190305.002/5240760:user/release-keys 339 """ 340 os.chdir(image_dir) 341 args = ['grep', 342 '-a', 343 'ro\..*build\.fingerprint=google/.*/release-keys', 344 'system.img'] 345 346 result, output = self._run_command( 347 args, 348 'Successfully extracted build fingerprint.', 349 'Build fingerprint extraction failed.') 350 os.chdir(self.working_dir) 351 if result: 352 _, fingerprint = output.split('=', 1) 353 return fingerprint.rstrip() 354 else: 355 return None 356 357 def _calculate_vbmeta_digest(self, image_dir): 358 """Calculates the VBMeta Digest for given partitions using avbtool. 359 360 Args: 361 image_dir: The folder containing the unpacked factory image partitions, 362 which contains a vbmeta.img partition. 363 364 Returns: 365 Hex string with the VBmeta Digest value or None if it failed. 366 """ 367 os.chdir(image_dir) 368 args = [self.avbtool_path, 369 'calculate_vbmeta_digest', 370 '--image', 'vbmeta.img'] 371 result, output = self._run_command(args, 372 'Successfully calculated VBMeta Digest.', 373 'Failed to calculate VBmeta Digest.') 374 os.chdir(self.working_dir) 375 if result: 376 return output 377 else: 378 return None 379 380 def _run_command(self, args, success_msg, fail_msg): 381 """Runs command line tools.""" 382 p = subprocess.Popen(args, stdin=subprocess.PIPE, 383 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 384 encoding='utf-8') 385 pout, _ = p.communicate() 386 if p.wait() == 0: 387 print(success_msg) 388 return True, pout 389 else: 390 print(fail_msg) 391 return False, pout 392 393 394if __name__ == '__main__': 395 tool = PixelFactoryImageVerifier() 396 tool.run(sys.argv) 397 398