Home | History | Annotate | Download | only in privapp_permissions
      1 #!/usr/bin/env python
      2 #
      3 #   Copyright 2017 - The Android Open Source Project
      4 #
      5 #   Licensed under the Apache License, Version 2.0 (the "License");
      6 #   you may not use this file except in compliance with the License.
      7 #   You may obtain a copy of the License at
      8 #
      9 #       http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 #   Unless required by applicable law or agreed to in writing, software
     12 #   distributed under the License is distributed on an "AS IS" BASIS,
     13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 #   See the License for the specific language governing permissions and
     15 #   limitations under the License.
     16 
     17 from __future__ import print_function
     18 from xml.dom import minidom
     19 
     20 import argparse
     21 import itertools
     22 import os
     23 import re
     24 import subprocess
     25 import sys
     26 import tempfile
     27 import shutil
     28 
     29 DEVICE_PREFIX = 'device:'
     30 ANDROID_NAME_REGEX = r'A: android:name\([\S]+\)=\"([\S]+)\"'
     31 ANDROID_PROTECTION_LEVEL_REGEX = \
     32     r'A: android:protectionLevel\([^\)]+\)=\(type [\S]+\)0x([\S]+)'
     33 BASE_XML_FILENAME = 'privapp-permissions-platform.xml'
     34 
     35 HELP_MESSAGE = """\
     36 Generates privapp-permissions.xml file for priv-apps.
     37 
     38 Usage:
     39     Specify which apk to generate priv-app permissions for. If no apk is \
     40 specified, this will default to all APKs under "<ANDROID_PRODUCT_OUT>/ \
     41 system/priv-app".
     42 
     43 Examples:
     44 
     45     For all APKs under $ANDROID_PRODUCT_OUT:
     46         # If the build environment has not been set up, do so:
     47         . build/envsetup.sh
     48         lunch product_name
     49         m -j32
     50         # then use:
     51         cd development/tools/privapp_permissions/
     52         ./privapp_permissions.py
     53 
     54     For a given apk:
     55         ./privapp_permissions.py path/to/the.apk
     56 
     57     For an APK already on the device:
     58         ./privapp_permissions.py device:/device/path/to/the.apk
     59 
     60     For all APKs on a device:
     61         ./privapp_permissions.py -d
     62         # or if more than one device is attached
     63         ./privapp_permissions.py -s <ANDROID_SERIAL>\
     64 """
     65 
     66 # An array of all generated temp directories.
     67 temp_dirs = []
     68 # An array of all generated temp files.
     69 temp_files = []
     70 
     71 
     72 class MissingResourceError(Exception):
     73     """Raised when a dependency cannot be located."""
     74 
     75 
     76 class Adb(object):
     77     """A small wrapper around ADB calls."""
     78 
     79     def __init__(self, path, serial=None):
     80         self.path = path
     81         self.serial = serial
     82 
     83     def pull(self, src, dst=None):
     84         """A wrapper for `adb -s <SERIAL> pull <src> <dst>`.
     85         Args:
     86             src: The source path on the device
     87             dst: The destination path on the host
     88 
     89         Throws:
     90             subprocess.CalledProcessError upon pull failure.
     91         """
     92         if not dst:
     93             if self.call('shell \'if [ -d "%s" ]; then echo True; fi\'' % src):
     94                 dst = tempfile.mkdtemp()
     95                 temp_dirs.append(dst)
     96             else:
     97                 _, dst = tempfile.mkstemp()
     98                 temp_files.append(dst)
     99         self.call('pull %s %s' % (src, dst))
    100         return dst
    101 
    102     def call(self, cmdline):
    103         """Calls an adb command.
    104 
    105         Throws:
    106             subprocess.CalledProcessError upon command failure.
    107         """
    108         command = '%s -s %s %s' % (self.path, self.serial, cmdline)
    109         return get_output(command)
    110 
    111 
    112 class Aapt(object):
    113     def __init__(self, path):
    114         self.path = path
    115 
    116     def call(self, arguments):
    117         """Run an aapt command with the given args.
    118 
    119         Args:
    120             arguments: a list of string arguments
    121         Returns:
    122             The output of the aapt command as a string.
    123         """
    124         output = subprocess.check_output([self.path] + arguments,
    125                                          stderr=subprocess.STDOUT)
    126         return output.decode(encoding='UTF-8')
    127 
    128 
    129 class Resources(object):
    130     """A class that contains the resources needed to generate permissions.
    131 
    132     Attributes:
    133         adb: A wrapper class around ADB with a default serial. Only needed when
    134              using -d, -s, or "device:"
    135         _aapt_path: The path to aapt.
    136     """
    137 
    138     def __init__(self, adb_path=None, aapt_path=None, use_device=None,
    139                  serial=None, apks=None):
    140         self.adb = Resources._resolve_adb(adb_path)
    141         self.aapt = Resources._resolve_aapt(aapt_path)
    142 
    143         self._is_android_env = 'ANDROID_PRODUCT_OUT' in os.environ and \
    144                                'ANDROID_HOST_OUT' in os.environ
    145         use_device = use_device or serial or \
    146                      (apks and DEVICE_PREFIX in '&'.join(apks))
    147 
    148         self.adb.serial = self._resolve_serial(use_device, serial)
    149 
    150         if self.adb.serial:
    151             self.adb.call('root')
    152             self.adb.call('wait-for-device')
    153 
    154         if self.adb.serial is None and not self._is_android_env:
    155             raise MissingResourceError(
    156                 'You must either set up your build environment, or specify a '
    157                 'device to run against. See --help for more info.')
    158 
    159         self.privapp_apks = self._resolve_apks(apks)
    160         self.permissions_dir = self._resolve_sys_path('system/etc/permissions')
    161         self.sysconfig_dir = self._resolve_sys_path('system/etc/sysconfig')
    162         self.framework_res_apk = self._resolve_sys_path('system/framework/'
    163                                                         'framework-res.apk')
    164 
    165     @staticmethod
    166     def _resolve_adb(adb_path):
    167         """Resolves ADB from either the cmdline argument or the os environment.
    168 
    169         Args:
    170             adb_path: The argument passed in for adb. Can be None.
    171         Returns:
    172             An Adb object.
    173         Raises:
    174             MissingResourceError if adb cannot be resolved.
    175         """
    176         if adb_path:
    177             if os.path.isfile(adb_path):
    178                 adb = adb_path
    179             else:
    180                 raise MissingResourceError('Cannot resolve adb: No such file '
    181                                            '"%s" exists.' % adb_path)
    182         else:
    183             try:
    184                 adb = get_output('which adb').strip()
    185             except subprocess.CalledProcessError as e:
    186                 print('Cannot resolve adb: ADB does not exist within path. '
    187                       'Did you forget to setup the build environment or set '
    188                       '--adb?',
    189                       file=sys.stderr)
    190                 raise MissingResourceError(e)
    191         # Start the adb server immediately so server daemon startup
    192         # does not get added to the output of subsequent adb calls.
    193         try:
    194             get_output('%s start-server' % adb)
    195             return Adb(adb)
    196         except:
    197             print('Unable to reach adb server daemon.', file=sys.stderr)
    198             raise
    199 
    200     @staticmethod
    201     def _resolve_aapt(aapt_path):
    202         """Resolves AAPT from either the cmdline argument or the os environment.
    203 
    204         Returns:
    205             An Aapt Object
    206         """
    207         if aapt_path:
    208             if os.path.isfile(aapt_path):
    209                 return Aapt(aapt_path)
    210             else:
    211                 raise MissingResourceError('Cannot resolve aapt: No such file '
    212                                            '%s exists.' % aapt_path)
    213         else:
    214             try:
    215                 return Aapt(get_output('which aapt').strip())
    216             except subprocess.CalledProcessError:
    217                 print('Cannot resolve aapt: AAPT does not exist within path. '
    218                       'Did you forget to setup the build environment or set '
    219                       '--aapt?',
    220                       file=sys.stderr)
    221                 raise
    222 
    223     def _resolve_serial(self, device, serial):
    224         """Resolves the serial used for device files or generating permissions.
    225 
    226         Returns:
    227             If -s/--serial is specified, it will return that serial.
    228             If -d or device: is found, it will grab the only available device.
    229             If there are multiple devices, it will use $ANDROID_SERIAL.
    230         Raises:
    231             MissingResourceError if the resolved serial would not be usable.
    232             subprocess.CalledProcessError if a command error occurs.
    233         """
    234         if device:
    235             if serial:
    236                 try:
    237                     output = get_output('%s -s %s get-state' %
    238                                         (self.adb.path, serial))
    239                 except subprocess.CalledProcessError:
    240                     raise MissingResourceError(
    241                         'Received error when trying to get the state of '
    242                         'device with serial "%s". Is it connected and in '
    243                         'device mode?' % serial)
    244                 if 'device' not in output:
    245                     raise MissingResourceError(
    246                         'Device "%s" is not in device mode. Reboot the phone '
    247                         'into device mode and try again.' % serial)
    248                 return serial
    249 
    250             elif 'ANDROID_SERIAL' in os.environ:
    251                 serial = os.environ['ANDROID_SERIAL']
    252                 command = '%s -s %s get-state' % (self.adb, serial)
    253                 try:
    254                     output = get_output(command)
    255                 except subprocess.CalledProcessError:
    256                     raise MissingResourceError(
    257                         'Device with serial $ANDROID_SERIAL ("%s") not '
    258                         'found.' % serial)
    259                 if 'device' in output:
    260                     return serial
    261                 raise MissingResourceError(
    262                     'Device with serial $ANDROID_SERIAL ("%s") was '
    263                     'found, but was not in the "device" state.')
    264 
    265             # Parses `adb devices` so it only returns a string of serials.
    266             get_serials_cmd = ('%s devices | tail -n +2 | head -n -1 | '
    267                                'cut -f1' % self.adb.path)
    268             try:
    269                 output = get_output(get_serials_cmd)
    270                 # If multiple serials appear in the output, raise an error.
    271                 if len(output.split()) > 1:
    272                     raise MissingResourceError(
    273                         'Multiple devices are connected. You must specify '
    274                         'which device to run against with flag --serial.')
    275                 return output.strip()
    276             except subprocess.CalledProcessError:
    277                 print('Unexpected error when querying for connected '
    278                       'devices.', file=sys.stderr)
    279                 raise
    280 
    281     def _resolve_apks(self, apks):
    282         """Resolves all APKs to run against.
    283 
    284         Returns:
    285             If no apk is specified in the arguments, return all apks in
    286             system/priv-app. Otherwise, returns a list with the specified apk.
    287         Throws:
    288             MissingResourceError if the specified apk or system/priv-app cannot
    289             be found.
    290         """
    291         if not apks:
    292             return self._resolve_all_privapps()
    293 
    294         ret_apks = []
    295         for apk in apks:
    296             if apk.startswith(DEVICE_PREFIX):
    297                 device_apk = apk[len(DEVICE_PREFIX):]
    298                 try:
    299                     apk = self.adb.pull(device_apk)
    300                 except subprocess.CalledProcessError:
    301                     raise MissingResourceError(
    302                         'File "%s" could not be located on device "%s".' %
    303                         (device_apk, self.adb.serial))
    304                 ret_apks.append(apk)
    305             elif not os.path.isfile(apk):
    306                 raise MissingResourceError('File "%s" does not exist.' % apk)
    307             else:
    308                 ret_apks.append(apk)
    309         return ret_apks
    310 
    311     def _resolve_all_privapps(self):
    312         """Extract package name and requested permissions."""
    313         if self._is_android_env:
    314             priv_app_dir = os.path.join(os.environ['ANDROID_PRODUCT_OUT'],
    315                                         'system/priv-app')
    316         else:
    317             try:
    318                 priv_app_dir = self.adb.pull('/system/priv-app/')
    319             except subprocess.CalledProcessError:
    320                 raise MissingResourceError(
    321                     'Directory "/system/priv-app" could not be pulled from on '
    322                     'device "%s".' % self.adb.serial)
    323 
    324         return get_output('find %s -name "*.apk"' % priv_app_dir).split()
    325 
    326     def _resolve_sys_path(self, file_path):
    327         """Resolves a path that is a part of an Android System Image."""
    328         if self._is_android_env:
    329             return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], file_path)
    330         else:
    331             return self.adb.pull(file_path)
    332 
    333 
    334 def get_output(command):
    335     """Returns the output of the command as a string.
    336 
    337     Throws:
    338         subprocess.CalledProcessError if exit status is non-zero.
    339     """
    340     output = subprocess.check_output(command, shell=True)
    341     # For Python3.4, decode the byte string so it is usable.
    342     return output.decode(encoding='UTF-8')
    343 
    344 
    345 def parse_args():
    346     """Parses the CLI."""
    347     parser = argparse.ArgumentParser(
    348         description=HELP_MESSAGE,
    349         formatter_class=argparse.RawDescriptionHelpFormatter)
    350     parser.add_argument(
    351         '-d',
    352         '--device',
    353         action='store_true',
    354         default=False,
    355         required=False,
    356         help='Whether or not to generate the privapp_permissions file for the '
    357              'build already on a device. See -s/--serial below for more '
    358              'details.'
    359     )
    360     parser.add_argument(
    361         '--adb',
    362         type=str,
    363         required=False,
    364         metavar='<ADB_PATH',
    365         help='Path to adb. If none specified, uses the environment\'s adb.'
    366     )
    367     parser.add_argument(
    368         '--aapt',
    369         type=str,
    370         required=False,
    371         metavar='<AAPT_PATH>',
    372         help='Path to aapt. If none specified, uses the environment\'s aapt.'
    373     )
    374     parser.add_argument(
    375         '-s',
    376         '--serial',
    377         type=str,
    378         required=False,
    379         metavar='<SERIAL>',
    380         help='The serial of the device to generate permissions for. If no '
    381              'serial is given, it will pick the only device connected over '
    382              'adb. If multiple devices are found, it will default to '
    383              '$ANDROID_SERIAL. Otherwise, the program will exit with error '
    384              'code 1. If -s is given, -d is not needed.'
    385     )
    386     parser.add_argument(
    387         'apks',
    388         nargs='*',
    389         type=str,
    390         help='A list of paths to priv-app APKs to generate permissions for. '
    391              'To make a path device-side, prefix the path with "device:".'
    392     )
    393     cmd_args = parser.parse_args()
    394 
    395     return cmd_args
    396 
    397 
    398 def create_permission_file(resources):
    399     # Parse base XML files in /etc dir, permissions listed there don't have
    400     # to be re-added
    401     base_permissions = {}
    402     base_xml_files = itertools.chain(list_xml_files(resources.permissions_dir),
    403                                      list_xml_files(resources.sysconfig_dir))
    404     for xml_file in base_xml_files:
    405         parse_config_xml(xml_file, base_permissions)
    406 
    407     priv_permissions = extract_priv_permissions(resources.aapt,
    408                                                 resources.framework_res_apk)
    409 
    410     apps_redefine_base = []
    411     results = {}
    412     for priv_app in resources.privapp_apks:
    413         pkg_info = extract_pkg_and_requested_permissions(resources.aapt,
    414                                                          priv_app)
    415         pkg_name = pkg_info['package_name']
    416         priv_perms = get_priv_permissions(pkg_info['permissions'],
    417                                           priv_permissions)
    418         # Compute diff against permissions defined in base file
    419         if base_permissions and (pkg_name in base_permissions):
    420             base_permissions_pkg = base_permissions[pkg_name]
    421             priv_perms = remove_base_permissions(priv_perms,
    422                                                  base_permissions_pkg)
    423             if priv_perms:
    424                 apps_redefine_base.append(pkg_name)
    425         if priv_perms:
    426             results[pkg_name] = sorted(priv_perms)
    427 
    428     print_xml(results, apps_redefine_base)
    429 
    430 
    431 def print_xml(results, apps_redefine_base, fd=sys.stdout):
    432     """Print results to the given file."""
    433     fd.write('<?xml version="1.0" encoding="utf-8"?>\n<permissions>\n')
    434     for package_name in sorted(results):
    435         if package_name in apps_redefine_base:
    436             fd.write('    <!-- Additional permissions on top of %s -->\n' %
    437                      BASE_XML_FILENAME)
    438         fd.write('    <privapp-permissions package="%s">\n' % package_name)
    439         for p in results[package_name]:
    440             fd.write('        <permission name="%s"/>\n' % p)
    441         fd.write('    </privapp-permissions>\n')
    442         fd.write('\n')
    443 
    444     fd.write('</permissions>\n')
    445 
    446 
    447 def remove_base_permissions(priv_perms, base_perms):
    448     """Removes set of base_perms from set of priv_perms."""
    449     if (not priv_perms) or (not base_perms):
    450         return priv_perms
    451     return set(priv_perms) - set(base_perms)
    452 
    453 
    454 def get_priv_permissions(requested_perms, priv_perms):
    455     """Return only permissions that are in priv_perms set."""
    456     return set(requested_perms).intersection(set(priv_perms))
    457 
    458 
    459 def list_xml_files(directory):
    460     """Returns a list of all .xml files within a given directory.
    461 
    462     Args:
    463         directory: the directory to look for xml files in.
    464     """
    465     xml_files = []
    466     for dirName, subdirList, file_list in os.walk(directory):
    467         for file in file_list:
    468             if file.endswith('.xml'):
    469                 file_path = os.path.join(dirName, file)
    470                 xml_files.append(file_path)
    471     return xml_files
    472 
    473 
    474 def extract_pkg_and_requested_permissions(aapt, apk_path):
    475     """
    476     Extract package name and list of requested permissions from the
    477     dump of manifest file
    478     """
    479     aapt_args = ['d', 'permissions', apk_path]
    480     txt = aapt.call(aapt_args)
    481 
    482     permissions = []
    483     package_name = None
    484     raw_lines = txt.split('\n')
    485     for line in raw_lines:
    486         regex = r"uses-permission.*: name='([\S]+)'"
    487         matches = re.search(regex, line)
    488         if matches:
    489             name = matches.group(1)
    490             permissions.append(name)
    491         regex = r'package: ([\S]+)'
    492         matches = re.search(regex, line)
    493         if matches:
    494             package_name = matches.group(1)
    495 
    496     return {'package_name': package_name, 'permissions': permissions}
    497 
    498 
    499 def extract_priv_permissions(aapt, apk_path):
    500     """Extract signature|privileged permissions from dump of manifest file."""
    501     aapt_args = ['d', 'xmltree', apk_path, 'AndroidManifest.xml']
    502     txt = aapt.call(aapt_args)
    503     raw_lines = txt.split('\n')
    504     n = len(raw_lines)
    505     i = 0
    506     permissions_list = []
    507     while i < n:
    508         line = raw_lines[i]
    509         if line.find('E: permission (') != -1:
    510             i += 1
    511             name = None
    512             level = None
    513             while i < n:
    514                 line = raw_lines[i]
    515                 if line.find('E: ') != -1:
    516                     break
    517                 matches = re.search(ANDROID_NAME_REGEX, line)
    518                 if matches:
    519                     name = matches.group(1)
    520                     i += 1
    521                     continue
    522                 matches = re.search(ANDROID_PROTECTION_LEVEL_REGEX, line)
    523                 if matches:
    524                     level = int(matches.group(1), 16)
    525                     i += 1
    526                     continue
    527                 i += 1
    528             if name and level and level & 0x12 == 0x12:
    529                 permissions_list.append(name)
    530         else:
    531             i += 1
    532 
    533     return permissions_list
    534 
    535 
    536 def parse_config_xml(base_xml, results):
    537     """Parse an XML file that will be used as base."""
    538     dom = minidom.parse(base_xml)
    539     nodes = dom.getElementsByTagName('privapp-permissions')
    540     for node in nodes:
    541         permissions = (node.getElementsByTagName('permission') +
    542                        node.getElementsByTagName('deny-permission'))
    543         package_name = node.getAttribute('package')
    544         plist = []
    545         if package_name in results:
    546             plist = results[package_name]
    547         for p in permissions:
    548             perm_name = p.getAttribute('name')
    549             if perm_name:
    550                 plist.append(perm_name)
    551         results[package_name] = plist
    552     return results
    553 
    554 
    555 def cleanup():
    556     """Cleans up temp files."""
    557     for directory in temp_dirs:
    558         shutil.rmtree(directory, ignore_errors=True)
    559     for file in temp_files:
    560         os.remove(file)
    561     del temp_dirs[:]
    562     del temp_files[:]
    563 
    564 
    565 if __name__ == '__main__':
    566     args = parse_args()
    567     try:
    568         tool_resources = Resources(
    569             aapt_path=args.aapt,
    570             adb_path=args.adb,
    571             use_device=args.device,
    572             serial=args.serial,
    573             apks=args.apks
    574         )
    575         create_permission_file(tool_resources)
    576     except MissingResourceError as e:
    577         print(str(e), file=sys.stderr)
    578         exit(1)
    579     finally:
    580         cleanup()
    581