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 17from __future__ import print_function 18from xml.dom import minidom 19 20import argparse 21import itertools 22import os 23import re 24import subprocess 25import sys 26import tempfile 27import shutil 28 29DEVICE_PREFIX = 'device:' 30ANDROID_NAME_REGEX = r'A: android:name\([\S]+\)=\"([\S]+)\"' 31ANDROID_PROTECTION_LEVEL_REGEX = \ 32 r'A: android:protectionLevel\([^\)]+\)=\(type [\S]+\)0x([\S]+)' 33BASE_XML_FILENAME = 'privapp-permissions-platform.xml' 34 35HELP_MESSAGE = """\ 36Generates privapp-permissions.xml file for priv-apps. 37 38Usage: 39 Specify which apk to generate priv-app permissions for. If no apk is \ 40specified, this will default to all APKs under "<ANDROID_PRODUCT_OUT>/ \ 41system/priv-app". 42 43Examples: 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. 67temp_dirs = [] 68# An array of all generated temp files. 69temp_files = [] 70 71 72class MissingResourceError(Exception): 73 """Raised when a dependency cannot be located.""" 74 75 76class 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 112class 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 129class 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 334def 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 345def 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 398def 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 431def 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 447def 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 454def 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 459def 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 474def 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 499def 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 536def 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 555def 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 565if __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