From 9066da53f5e9963dd0e1ae9a1e8925c0e4084f0e Mon Sep 17 00:00:00 2001 From: markdr Date: Mon, 11 Dec 2017 20:15:03 -0800 Subject: [PATCH] Made privapp_permissions.py more dynamic. This was done largely so automation can make use of privapp-permissions.xml generation. Privapp_permissions.py no longer requires the build environment to be setup. Instead, system files can be pulled from a connected device, and aapt & adb can be set via the commandline. Otherwise, they will default to the environment's default aapt/adb. Priv-apps and the permission lists can be queried from the device using command flags as well, removing the need for an Android image to be built locally. Bug: None Test: Diff between previous privapp_permissions output and the output for these changes was empty for the following runs: ./old_privapp_permissions.py vs ./new_privapp_permissions.py [-d && -s SERIAL] ./new_privapp_permissions.py --aapt /path/to/aapt ./old_privapp_permissions.py /path/to.apk vs ./new_privapp_permissions.py /path/to.apk ./new_privapp_permissions.py device:/path/to/apk Change-Id: Ifaf35607d38c1d74111fd2f05628c0192fc791cb --- tools/privapp_permissions/OWNERS | 2 + .../privapp_permissions.py | 614 ++++++++++++++---- 2 files changed, 486 insertions(+), 130 deletions(-) create mode 100644 tools/privapp_permissions/OWNERS diff --git a/tools/privapp_permissions/OWNERS b/tools/privapp_permissions/OWNERS new file mode 100644 index 000000000..3c0eadc7f --- /dev/null +++ b/tools/privapp_permissions/OWNERS @@ -0,0 +1,2 @@ +fkupolov@google.com +markdr@google.com diff --git a/tools/privapp_permissions/privapp_permissions.py b/tools/privapp_permissions/privapp_permissions.py index 4e6adf183..8a9510a16 100755 --- a/tools/privapp_permissions/privapp_permissions.py +++ b/tools/privapp_permissions/privapp_permissions.py @@ -1,67 +1,425 @@ #!/usr/bin/env python - # -# Copyright 2016, The Android Open Source Project +# Copyright 2017 - The Android Open Source Project # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# http://www.apache.org/licenses/LICENSE-2.0 # +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -""" - privapp_permission.py: Generates privapp-permissions.xml file for - apps in system/priv-app directory - - Usage: - . build/envsetup.sh - lunch product_name - m -j32 - development/tools/privapp_permissions/privapp_permissions.py [package_name] - -""" +from __future__ import print_function +from xml.dom import minidom +import argparse +import itertools import os import re import subprocess import sys -from xml.dom import minidom +import tempfile +import shutil -try: - ANDROID_PRODUCT_OUT = os.environ['ANDROID_PRODUCT_OUT'] - ANDROID_HOST_OUT = os.environ['ANDROID_HOST_OUT'] -except KeyError as e: - exit("Build environment not set up - " + str(e)) -BASE_XML_FNAME = "privapp-permissions-platform.xml" +DEVICE_PREFIX = 'device:' +ANDROID_NAME_REGEX = r'A: android:name\([\S]+\)=\"([\S]+)\"' +ANDROID_PROTECTION_LEVEL_REGEX = \ + r'A: android:protectionLevel\([^\)]+\)=\(type [\S]+\)0x([\S]+)' +BASE_XML_FILENAME = 'privapp-permissions-platform.xml' -def main(): - # Parse base XML files in /etc dir, permissions listed there don't have to be re-added +HELP_MESSAGE = """\ +Generates privapp-permissions.xml file for priv-apps. + +Usage: + Specify which apk to generate priv-app permissions for. If no apk is \ +specified, this will default to all APKs under "/ \ +system/priv-app". + +Examples: + + For all APKs under $ANDROID_PRODUCT_OUT: + # If the build environment has not been set up, do so: + . build/envsetup.sh + lunch product_name + m -j32 + # then use: + cd development/tools/privapp_permissions/ + ./privapp_permissions.py + + For a given apk: + ./privapp_permissions.py path/to/the.apk + + For an APK already on the device: + ./privapp_permissions.py device:/device/path/to/the.apk + + For all APKs on a device: + ./privapp_permissions.py -d + # or if more than one device is attached + ./privapp_permissions.py -s \ +""" + +# An array of all generated temp directories. +temp_dirs = [] +# An array of all generated temp files. +temp_files = [] + + +class MissingResourceError(Exception): + """Raised when a dependency cannot be located.""" + + +class Adb(object): + """A small wrapper around ADB calls.""" + + def __init__(self, path, serial=None): + self.path = path + self.serial = serial + + def pull(self, src, dst=None): + """A wrapper for `adb -s pull `. + Args: + src: The source path on the device + dst: The destination path on the host + + Throws: + subprocess.CalledProcessError upon pull failure. + """ + if not dst: + if self.call('shell \'if [ -d "%s" ]; then echo True; fi\'' % src): + dst = tempfile.mkdtemp() + temp_dirs.append(dst) + else: + _, dst = tempfile.mkstemp() + temp_files.append(dst) + self.call('pull %s %s' % (src, dst)) + return dst + + def call(self, cmdline): + """Calls an adb command. + + Throws: + subprocess.CalledProcessError upon command failure. + """ + command = '%s -s %s %s' % (self.path, self.serial, cmdline) + return get_output(command) + + +class Aapt(object): + def __init__(self, path): + self.path = path + + def call(self, arguments): + """Run an aapt command with the given args. + + Args: + arguments: a list of string arguments + Returns: + The output of the aapt command as a string. + """ + output = subprocess.check_output([self.path] + arguments, + stderr=subprocess.STDOUT) + return output.decode(encoding='UTF-8') + + +class Resources(object): + """A class that contains the resources needed to generate permissions. + + Attributes: + adb: A wrapper class around ADB with a default serial. Only needed when + using -d, -s, or "device:" + _aapt_path: The path to aapt. + """ + + def __init__(self, adb_path=None, aapt_path=None, use_device=None, + serial=None, apks=None): + self.adb = Resources._resolve_adb(adb_path) + self.aapt = Resources._resolve_aapt(aapt_path) + + self._is_android_env = 'ANDROID_PRODUCT_OUT' in os.environ and \ + 'ANDROID_HOST_OUT' in os.environ + use_device = use_device or serial or \ + (apks and DEVICE_PREFIX in '&'.join(apks)) + + self.adb.serial = self._resolve_serial(use_device, serial) + + if self.adb.serial: + self.adb.call('root') + self.adb.call('wait-for-device') + + if self.adb.serial is None and not self._is_android_env: + raise MissingResourceError( + 'You must either set up your build environment, or specify a ' + 'device to run against. See --help for more info.') + + self.privapp_apks = self._resolve_apks(apks) + self.permissions_dir = self._resolve_sys_path('system/etc/permissions') + self.sysconfig_dir = self._resolve_sys_path('system/etc/sysconfig') + self.framework_res_apk = self._resolve_sys_path('system/framework/' + 'framework-res.apk') + + @staticmethod + def _resolve_adb(adb_path): + """Resolves ADB from either the cmdline argument or the os environment. + + Args: + adb_path: The argument passed in for adb. Can be None. + Returns: + An Adb object. + Raises: + MissingResourceError if adb cannot be resolved. + """ + if adb_path: + if os.path.isfile(adb_path): + adb = adb_path + else: + raise MissingResourceError('Cannot resolve adb: No such file ' + '"%s" exists.' % adb_path) + else: + try: + adb = get_output('which adb').strip() + except subprocess.CalledProcessError as e: + print('Cannot resolve adb: ADB does not exist within path. ' + 'Did you forget to setup the build environment or set ' + '--adb?', + file=sys.stderr) + raise MissingResourceError(e) + # Start the adb server immediately so server daemon startup + # does not get added to the output of subsequent adb calls. + try: + get_output('%s start-server' % adb) + return Adb(adb) + except: + print('Unable to reach adb server daemon.', file=sys.stderr) + raise + + @staticmethod + def _resolve_aapt(aapt_path): + """Resolves AAPT from either the cmdline argument or the os environment. + + Returns: + An Aapt Object + """ + if aapt_path: + if os.path.isfile(aapt_path): + return Aapt(aapt_path) + else: + raise MissingResourceError('Cannot resolve aapt: No such file ' + '%s exists.' % aapt_path) + else: + try: + return Aapt(get_output('which aapt').strip()) + except subprocess.CalledProcessError: + print('Cannot resolve aapt: AAPT does not exist within path. ' + 'Did you forget to setup the build environment or set ' + '--aapt?', + file=sys.stderr) + raise + + def _resolve_serial(self, device, serial): + """Resolves the serial used for device files or generating permissions. + + Returns: + If -s/--serial is specified, it will return that serial. + If -d or device: is found, it will grab the only available device. + If there are multiple devices, it will use $ANDROID_SERIAL. + Raises: + MissingResourceError if the resolved serial would not be usable. + subprocess.CalledProcessError if a command error occurs. + """ + if device: + if serial: + try: + output = get_output('%s -s %s get-state' % + (self.adb.path, serial)) + except subprocess.CalledProcessError: + raise MissingResourceError( + 'Received error when trying to get the state of ' + 'device with serial "%s". Is it connected and in ' + 'device mode?' % serial) + if 'device' not in output: + raise MissingResourceError( + 'Device "%s" is not in device mode. Reboot the phone ' + 'into device mode and try again.' % serial) + return serial + + elif 'ANDROID_SERIAL' in os.environ: + serial = os.environ['ANDROID_SERIAL'] + command = '%s -s %s get-state' % (self.adb, serial) + try: + output = get_output(command) + except subprocess.CalledProcessError: + raise MissingResourceError( + 'Device with serial $ANDROID_SERIAL ("%s") not ' + 'found.' % serial) + if 'device' in output: + return serial + raise MissingResourceError( + 'Device with serial $ANDROID_SERIAL ("%s") was ' + 'found, but was not in the "device" state.') + + # Parses `adb devices` so it only returns a string of serials. + get_serials_cmd = ('%s devices | tail -n +2 | head -n -1 | ' + 'cut -f1' % self.adb.path) + try: + output = get_output(get_serials_cmd) + # If multiple serials appear in the output, raise an error. + if len(output.split()) > 1: + raise MissingResourceError( + 'Multiple devices are connected. You must specify ' + 'which device to run against with flag --serial.') + return output.strip() + except subprocess.CalledProcessError: + print('Unexpected error when querying for connected ' + 'devices.', file=sys.stderr) + raise + + def _resolve_apks(self, apks): + """Resolves all APKs to run against. + + Returns: + If no apk is specified in the arguments, return all apks in + system/priv-app. Otherwise, returns a list with the specified apk. + Throws: + MissingResourceError if the specified apk or system/priv-app cannot + be found. + """ + if not apks: + return self._resolve_all_privapps() + + ret_apks = [] + for apk in apks: + if apk.startswith(DEVICE_PREFIX): + device_apk = apk[len(DEVICE_PREFIX):] + try: + apk = self.adb.pull(device_apk) + except subprocess.CalledProcessError: + raise MissingResourceError( + 'File "%s" could not be located on device "%s".' % + (device_apk, self.adb.serial)) + ret_apks.append(apk) + elif not os.path.isfile(apk): + raise MissingResourceError('File "%s" does not exist.' % apk) + else: + ret_apks.append(apk) + return ret_apks + + def _resolve_all_privapps(self): + """Extract package name and requested permissions.""" + if self._is_android_env: + priv_app_dir = os.path.join(os.environ['ANDROID_PRODUCT_OUT'], + 'system/priv-app') + else: + try: + priv_app_dir = self.adb.pull('/system/priv-app/') + except subprocess.CalledProcessError: + raise MissingResourceError( + 'Directory "/system/priv-app" could not be pulled from on ' + 'device "%s".' % self.adb.serial) + + return get_output('find %s -name "*.apk"' % priv_app_dir).split() + + def _resolve_sys_path(self, file_path): + """Resolves a path that is a part of an Android System Image.""" + if self._is_android_env: + return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], file_path) + else: + return self.adb.pull(file_path) + + +def get_output(command): + """Returns the output of the command as a string. + + Throws: + subprocess.CalledProcessError if exit status is non-zero. + """ + output = subprocess.check_output(command, shell=True) + # For Python3.4, decode the byte string so it is usable. + return output.decode(encoding='UTF-8') + + +def parse_args(): + """Parses the CLI.""" + parser = argparse.ArgumentParser( + description=HELP_MESSAGE, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + '-d', + '--device', + action='store_true', + default=False, + required=False, + help='Whether or not to generate the privapp_permissions file for the ' + 'build already on a device. See -s/--serial below for more ' + 'details.' + ) + parser.add_argument( + '--adb', + type=str, + required=False, + metavar=' 1 else list_privapps() apps_redefine_base = [] results = {} - for priv_app in priv_apps: - pkg_info = extract_pkg_and_requested_permissions(priv_app) + for priv_app in resources.privapp_apks: + pkg_info = extract_pkg_and_requested_permissions(resources.aapt, + priv_app) pkg_name = pkg_info['package_name'] - priv_perms = get_priv_permissions(pkg_info['permissions'], platform_priv_permissions) + priv_perms = get_priv_permissions(pkg_info['permissions'], + priv_permissions) # Compute diff against permissions defined in base file if base_permissions and (pkg_name in base_permissions): base_permissions_pkg = base_permissions[pkg_name] - priv_perms = remove_base_permissions(priv_perms, base_permissions_pkg) + priv_perms = remove_base_permissions(priv_perms, + base_permissions_pkg) if priv_perms: apps_redefine_base.append(pkg_name) if priv_perms: @@ -69,142 +427,120 @@ def main(): print_xml(results, apps_redefine_base) -def print_xml(results, apps_redefine_base): - """ - Print results to xml file - """ - print """\ - -""" + +def print_xml(results, apps_redefine_base, fd=sys.stdout): + """Print results to the given file.""" + fd.write('\n\n') for package_name in sorted(results): if package_name in apps_redefine_base: - print ' ' % BASE_XML_FNAME - print ' ' % package_name + fd.write(' \n' % + BASE_XML_FILENAME) + fd.write(' \n' % package_name) for p in results[package_name]: - print ' ' % p - print ' ' - print + fd.write(' \n' % p) + fd.write(' \n') + fd.write('\n') + + fd.write('\n') - print "" def remove_base_permissions(priv_perms, base_perms): - """ - Removes set of base_perms from set of priv_perms - """ - if (not priv_perms) or (not base_perms): return priv_perms + """Removes set of base_perms from set of priv_perms.""" + if (not priv_perms) or (not base_perms): + return priv_perms return set(priv_perms) - set(base_perms) + def get_priv_permissions(requested_perms, priv_perms): - """ - Return only permissions that are in priv_perms set - """ + """Return only permissions that are in priv_perms set.""" return set(requested_perms).intersection(set(priv_perms)) -def list_privapps(): - """ - Extract package name and requested permissions. - """ - priv_app_dir = os.path.join(ANDROID_PRODUCT_OUT, 'system/priv-app') - apks = [] - for dirName, subdirList, fileList in os.walk(priv_app_dir): - for fname in fileList: - if fname.endswith(".apk"): - file_path = os.path.join(dirName, fname) - apks.append(file_path) - return apks +def list_xml_files(directory): + """Returns a list of all .xml files within a given directory. -def list_config_xml_files(): + Args: + directory: the directory to look for xml files in. """ - Extract package name and requested permissions. - """ - perm_dir = os.path.join(ANDROID_PRODUCT_OUT, 'system/etc/permissions') - conf_dir = os.path.join(ANDROID_PRODUCT_OUT, 'system/etc/sysconfig') - xml_files = [] - for root_dir in [perm_dir, conf_dir]: - for dirName, subdirList, fileList in os.walk(root_dir): - for fname in fileList: - if fname.endswith(".xml"): - file_path = os.path.join(dirName, fname); - xml_files.append(file_path) + for dirName, subdirList, file_list in os.walk(directory): + for file in file_list: + if file.endswith('.xml'): + file_path = os.path.join(dirName, file) + xml_files.append(file_path) return xml_files -def extract_pkg_and_requested_permissions(apk_path): +def extract_pkg_and_requested_permissions(aapt, apk_path): """ Extract package name and list of requested permissions from the dump of manifest file """ - aapt_args = ["d", "permissions", apk_path] - txt = aapt(aapt_args) + aapt_args = ['d', 'permissions', apk_path] + txt = aapt.call(aapt_args) permissions = [] package_name = None - rawLines = txt.split('\n') - for line in rawLines: + raw_lines = txt.split('\n') + for line in raw_lines: regex = r"uses-permission: name='([\S]+)'" matches = re.search(regex, line) if matches: name = matches.group(1) permissions.append(name) - regex = r"package: ([\S]+)" + regex = r'package: ([\S]+)' matches = re.search(regex, line) if matches: package_name = matches.group(1) - return {'package_name': package_name, 'permissions' : permissions} + return {'package_name': package_name, 'permissions': permissions} -def extract_priv_permissions(apk_path): - """ - Extract list signature|privileged permissions from the dump of - manifest file - """ - aapt_args = ["d", "xmltree", apk_path, "AndroidManifest.xml"] - txt = aapt(aapt_args) - rawLines = txt.split('\n') - n = len(rawLines) + +def extract_priv_permissions(aapt, apk_path): + """Extract signature|privileged permissions from dump of manifest file.""" + aapt_args = ['d', 'xmltree', apk_path, 'AndroidManifest.xml'] + txt = aapt.call(aapt_args) + raw_lines = txt.split('\n') + n = len(raw_lines) i = 0 permissions_list = [] - while i