398 lines
13 KiB
Python
Executable File
398 lines
13 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Copyright 2019, The Android Open Source Project
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation
|
|
# files (the "Software"), to deal in the Software without
|
|
# restriction, including without limitation the rights to use, copy,
|
|
# modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
# of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
#
|
|
|
|
"""Tool for verifying VBMeta & calculate VBMeta Digests of Pixel factory images.
|
|
|
|
If given an HTTPS URL it will download the file first before processing.
|
|
$ pixel_factory_image_verify.py https://dl.google.com/dl/android/aosp/image.zip
|
|
|
|
Otherwise, the argument is considered to be a local file.
|
|
$ pixel_factory_image_verify.py image.zip
|
|
|
|
The list of canonical Pixel factory images can be found here:
|
|
https://developers.google.com/android/images
|
|
|
|
Supported: all factory images of Pixel 6 and later devices.
|
|
|
|
In order for the tool to run correct the following utilities need to be
|
|
pre-installed: grep, wget or curl, unzip.
|
|
|
|
Additionally, make sure that the bootloader unpacker script is separately
|
|
downloaded, made executable, and symlinked as 'fbpacktool', and made accessible
|
|
via your shell $PATH.
|
|
|
|
The tool also runs outside of the repository location as long as the working
|
|
directory is writable.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
|
|
import glob
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import distutils.spawn
|
|
|
|
|
|
class PixelFactoryImageVerifier(object):
|
|
"""Object for the pixel_factory_image_verify command line tool."""
|
|
|
|
ERR_TOOL_UNAVAIL_FMT_STR = 'Necessary command line tool needs to be installed first: %s'
|
|
|
|
def __init__(self):
|
|
self.working_dir = os.getcwd()
|
|
self.script_path = os.path.realpath(__file__)
|
|
self.script_dir = os.path.split(self.script_path)[0]
|
|
self.avbtool_path = os.path.abspath(os.path.join(self.script_path,
|
|
'../../../avbtool'))
|
|
self.fw_unpacker_path = distutils.spawn.find_executable('fbpacktool')
|
|
self.wget_path = distutils.spawn.find_executable('wget')
|
|
self.curl_path = distutils.spawn.find_executable('curl')
|
|
|
|
def run(self, argv):
|
|
"""Command line processor.
|
|
|
|
Args:
|
|
argv: The command line parameter list.
|
|
"""
|
|
# Checks for command line parameters and show help if non given.
|
|
if len(argv) != 2:
|
|
print('No command line parameter given. At least a filename or URL for a '
|
|
'Pixel 3 or later factory image needs to be specified.')
|
|
sys.exit(1)
|
|
|
|
# Checks if necessary commands are available.
|
|
for cmd in ['grep', 'unzip']:
|
|
if not distutils.spawn.find_executable(cmd):
|
|
print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % cmd)
|
|
sys.exit(1)
|
|
|
|
# Checks if `fbpacktool` is available.
|
|
if not self.fw_unpacker_path:
|
|
print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'fbpacktool')
|
|
sys.exit(1)
|
|
|
|
# Checks if either `wget` or `curl` is available.
|
|
if not self.wget_path and not self.curl_path:
|
|
print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'wget or curl')
|
|
sys.exit(1)
|
|
|
|
# Downloads factory image if URL is specified; otherwise treat it as file.
|
|
if argv[1].lower().startswith('https://'):
|
|
factory_image_zip = self._download_factory_image(argv[1])
|
|
if not factory_image_zip:
|
|
sys.exit(1)
|
|
else:
|
|
factory_image_zip = os.path.abspath(argv[1])
|
|
|
|
# Unpacks the factory image into partition images.
|
|
partition_image_dir = self._unpack_factory_image(factory_image_zip)
|
|
if not partition_image_dir:
|
|
sys.exit(1)
|
|
|
|
# Unpacks bootloader image into individual component images.
|
|
unpack_successful = self._unpack_bootloader(partition_image_dir)
|
|
if not unpack_successful:
|
|
sys.exit(1)
|
|
|
|
# Validates the VBMeta of the factory image.
|
|
verified = self._verify_vbmeta_partitions(partition_image_dir)
|
|
if not verified:
|
|
sys.exit(1)
|
|
|
|
fingerprint = self._extract_build_fingerprint(partition_image_dir)
|
|
if not fingerprint:
|
|
sys.exit(1)
|
|
|
|
# Calculates the VBMeta Digest for the factory image.
|
|
vbmeta_digest = self._calculate_vbmeta_digest(partition_image_dir)
|
|
if not vbmeta_digest:
|
|
sys.exit(1)
|
|
|
|
print('The build fingerprint for factory image is: %s' % fingerprint)
|
|
print('The VBMeta Digest for factory image is: %s' % vbmeta_digest)
|
|
|
|
with open('payload.txt', 'w') as f_out:
|
|
f_out.write(fingerprint.strip() + '\n')
|
|
f_out.write(vbmeta_digest.strip() + '\n')
|
|
print('A corresponding "payload.txt" file has been created.')
|
|
sys.exit(0)
|
|
|
|
def _download_factory_image(self, url):
|
|
"""Downloads the factory image to the working directory.
|
|
|
|
Args:
|
|
url: The download URL for the factory image.
|
|
|
|
Returns:
|
|
The absolute path to the factory image or None if it failed.
|
|
"""
|
|
# Creates temporary download folder.
|
|
download_path = tempfile.mkdtemp(dir=self.working_dir)
|
|
|
|
# Downloads the factory image to the temporary folder.
|
|
download_filename = self._download_file(download_path, url)
|
|
if not download_filename:
|
|
return None
|
|
|
|
# Moves the downloaded file into the working directory.
|
|
download_file = os.path.join(download_path, download_filename)
|
|
target_file = os.path.join(self.working_dir, download_filename)
|
|
if os.path.exists(target_file):
|
|
try:
|
|
os.remove(target_file)
|
|
except OSError as e:
|
|
print('File %s already exists and cannot be deleted.' % download_file)
|
|
return None
|
|
try:
|
|
shutil.move(download_file, self.working_dir)
|
|
except shutil.Error as e:
|
|
print('File %s cannot be moved to %s: %s' % (download_file,
|
|
target_file, e))
|
|
return None
|
|
|
|
# Removes temporary download folder.
|
|
try:
|
|
shutil.rmtree(download_path)
|
|
except shutil.Error as e:
|
|
print('Temporary download folder %s could not be removed.'
|
|
% download_path)
|
|
return os.path.join(self.working_dir, download_filename)
|
|
|
|
def _download_file(self, download_dir, url):
|
|
"""Downloads a file from the Internet.
|
|
|
|
Args:
|
|
download_dir: The folder the file should be downloaded to.
|
|
url: The download URL for the file.
|
|
|
|
Returns:
|
|
The name of the downloaded file as it apears on disk; otherwise None
|
|
if download failed.
|
|
"""
|
|
print('Fetching file from: %s' % url)
|
|
os.chdir(download_dir)
|
|
args = []
|
|
if self.wget_path:
|
|
args = [self.wget_path, url]
|
|
else:
|
|
args = [self.curl_path, '-O', url]
|
|
|
|
result, _ = self._run_command(args,
|
|
'Successfully downloaded file.',
|
|
'File download failed.')
|
|
os.chdir(self.working_dir)
|
|
if not result:
|
|
return None
|
|
|
|
# Figure out the file name of what was downloaded: It will be the only file
|
|
# in the download folder.
|
|
files = os.listdir(download_dir)
|
|
if files and len(files) == 1:
|
|
return files[0]
|
|
else:
|
|
return None
|
|
|
|
def _unpack_bootloader(self, factory_image_folder):
|
|
"""Unpacks the bootloader to produce individual images.
|
|
|
|
Args:
|
|
factory_image_folder: path to the directory containing factory images.
|
|
|
|
Returns:
|
|
True if unpack is successful. False if otherwise.
|
|
"""
|
|
os.chdir(factory_image_folder)
|
|
bootloader_path = os.path.join(factory_image_folder, 'bootloader*.img')
|
|
glob_result = glob.glob(bootloader_path)
|
|
if not glob_result:
|
|
return False
|
|
|
|
args = [self.fw_unpacker_path, 'unpack', glob_result[0]]
|
|
result, _ = self._run_command(args,
|
|
'Successfully unpacked bootloader image.',
|
|
'Failed to unpack bootloader image.')
|
|
return result
|
|
|
|
def _unpack_factory_image(self, factory_image_file):
|
|
"""Unpacks the factory image zip file.
|
|
|
|
Args:
|
|
factory_image_file: path and file name to the image file.
|
|
|
|
Returns:
|
|
The path to the folder which contains the unpacked factory image files or
|
|
None if it failed.
|
|
"""
|
|
unpack_dir = tempfile.mkdtemp(dir=self.working_dir)
|
|
args = ['unzip', factory_image_file, '-d', unpack_dir]
|
|
result, _ = self._run_command(args,
|
|
'Successfully unpacked factory image.',
|
|
'Failed to unpack factory image.')
|
|
if not result:
|
|
return None
|
|
|
|
# Locate the directory which contains the image files.
|
|
files = os.listdir(unpack_dir)
|
|
image_name = None
|
|
for f in files:
|
|
path = os.path.join(self.working_dir, unpack_dir, f)
|
|
if os.path.isdir(path):
|
|
image_name = f
|
|
break
|
|
if not image_name:
|
|
print('No image found: %s' % image_name)
|
|
return None
|
|
|
|
# Move image file directory to the working directory
|
|
image_dir = os.path.join(unpack_dir, image_name)
|
|
target_dir = os.path.join(self.working_dir, image_name)
|
|
if os.path.exists(target_dir):
|
|
try:
|
|
shutil.rmtree(target_dir)
|
|
except shutil.Error as e:
|
|
print('Directory %s already exists and cannot be deleted.' % target_dir)
|
|
return None
|
|
|
|
try:
|
|
shutil.move(image_dir, self.working_dir)
|
|
except shutil.Error as e:
|
|
print('Directory %s could not be moved to %s: %s' % (image_dir,
|
|
self.working_dir, e))
|
|
return None
|
|
|
|
# Removes tmp unpack directory.
|
|
try:
|
|
shutil.rmtree(unpack_dir)
|
|
except shutil.Error as e:
|
|
print('Temporary download folder %s could not be removed.'
|
|
% unpack_dir)
|
|
|
|
# Unzip the secondary zip file which contain the individual images.
|
|
image_filename = 'image-%s' % image_name
|
|
image_folder = os.path.join(self.working_dir, image_name)
|
|
os.chdir(image_folder)
|
|
|
|
args = ['unzip', image_filename]
|
|
result, _ = self._run_command(
|
|
args,
|
|
'Successfully unpacked factory image partitions.',
|
|
'Failed to unpack factory image partitions.')
|
|
if not result:
|
|
return None
|
|
return image_folder
|
|
|
|
def _verify_vbmeta_partitions(self, image_dir):
|
|
"""Verifies all partitions protected by VBMeta using avbtool verify_image.
|
|
|
|
Args:
|
|
image_dir: The folder containing the unpacked factory image partitions,
|
|
which contains a vbmeta.img patition.
|
|
|
|
Returns:
|
|
True if the VBMeta protected partitions verify.
|
|
"""
|
|
os.chdir(image_dir)
|
|
args = [self.avbtool_path,
|
|
'verify_image',
|
|
'--image', 'vbmeta.img',
|
|
'--follow_chain_partitions']
|
|
result, _ = self._run_command(args,
|
|
'Successfully verified VBmeta.',
|
|
'Verification of VBmeta failed.')
|
|
os.chdir(self.working_dir)
|
|
return result
|
|
|
|
def _extract_build_fingerprint(self, image_dir):
|
|
"""Extracts the build fingerprint from the system.img.
|
|
Args:
|
|
image_dir: The folder containing the unpacked factory image partitions,
|
|
which contains a vbmeta.img patition.
|
|
|
|
Returns:
|
|
The build fingerprint string, e.g.
|
|
google/blueline/blueline:9/PQ2A.190305.002/5240760:user/release-keys
|
|
"""
|
|
os.chdir(image_dir)
|
|
args = ['grep',
|
|
'-a',
|
|
'ro\..*build\.fingerprint=google/.*/release-keys',
|
|
'system.img']
|
|
|
|
result, output = self._run_command(
|
|
args,
|
|
'Successfully extracted build fingerprint.',
|
|
'Build fingerprint extraction failed.')
|
|
os.chdir(self.working_dir)
|
|
if result:
|
|
_, fingerprint = output.split('=', 1)
|
|
return fingerprint.rstrip()
|
|
else:
|
|
return None
|
|
|
|
def _calculate_vbmeta_digest(self, image_dir):
|
|
"""Calculates the VBMeta Digest for given partitions using avbtool.
|
|
|
|
Args:
|
|
image_dir: The folder containing the unpacked factory image partitions,
|
|
which contains a vbmeta.img partition.
|
|
|
|
Returns:
|
|
Hex string with the VBmeta Digest value or None if it failed.
|
|
"""
|
|
os.chdir(image_dir)
|
|
args = [self.avbtool_path,
|
|
'calculate_vbmeta_digest',
|
|
'--image', 'vbmeta.img']
|
|
result, output = self._run_command(args,
|
|
'Successfully calculated VBMeta Digest.',
|
|
'Failed to calculate VBmeta Digest.')
|
|
os.chdir(self.working_dir)
|
|
if result:
|
|
return output
|
|
else:
|
|
return None
|
|
|
|
def _run_command(self, args, success_msg, fail_msg):
|
|
"""Runs command line tools."""
|
|
p = subprocess.Popen(args, stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
encoding='utf-8')
|
|
pout, _ = p.communicate()
|
|
if p.wait() == 0:
|
|
print(success_msg)
|
|
return True, pout
|
|
else:
|
|
print(fail_msg)
|
|
return False, pout
|
|
|
|
|
|
if __name__ == '__main__':
|
|
tool = PixelFactoryImageVerifier()
|
|
tool.run(sys.argv)
|
|
|