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 and (system/)product/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.system_privapp_apks, self.product_privapp_apks =( 160 self._resolve_apks(apks)) 161 self.system_permissions_dir = ( 162 self._resolve_sys_path('system/etc/permissions')) 163 self.system_sysconfig_dir = ( 164 self._resolve_sys_path('system/etc/sysconfig')) 165 self.product_permissions_dir = ( 166 self._resolve_sys_path('product/etc/permissions', 167 'system/product/etc/permissions')) 168 self.product_sysconfig_dir = ( 169 self._resolve_sys_path('product/etc/sysconfig', 170 'system/product/etc/sysconfig')) 171 self.framework_res_apk = self._resolve_sys_path('system/framework/' 172 'framework-res.apk') 173 174 @staticmethod 175 def _resolve_adb(adb_path): 176 """Resolves ADB from either the cmdline argument or the os environment. 177 178 Args: 179 adb_path: The argument passed in for adb. Can be None. 180 Returns: 181 An Adb object. 182 Raises: 183 MissingResourceError if adb cannot be resolved. 184 """ 185 if adb_path: 186 if os.path.isfile(adb_path): 187 adb = adb_path 188 else: 189 raise MissingResourceError('Cannot resolve adb: No such file ' 190 '"%s" exists.' % adb_path) 191 else: 192 try: 193 adb = get_output('which adb').strip() 194 except subprocess.CalledProcessError as e: 195 print('Cannot resolve adb: ADB does not exist within path. ' 196 'Did you forget to setup the build environment or set ' 197 '--adb?', 198 file=sys.stderr) 199 raise MissingResourceError(e) 200 # Start the adb server immediately so server daemon startup 201 # does not get added to the output of subsequent adb calls. 202 try: 203 get_output('%s start-server' % adb) 204 return Adb(adb) 205 except: 206 print('Unable to reach adb server daemon.', file=sys.stderr) 207 raise 208 209 @staticmethod 210 def _resolve_aapt(aapt_path): 211 """Resolves AAPT from either the cmdline argument or the os environment. 212 213 Returns: 214 An Aapt Object 215 """ 216 if aapt_path: 217 if os.path.isfile(aapt_path): 218 return Aapt(aapt_path) 219 else: 220 raise MissingResourceError('Cannot resolve aapt: No such file ' 221 '%s exists.' % aapt_path) 222 else: 223 try: 224 return Aapt(get_output('which aapt').strip()) 225 except subprocess.CalledProcessError: 226 print('Cannot resolve aapt: AAPT does not exist within path. ' 227 'Did you forget to setup the build environment or set ' 228 '--aapt?', 229 file=sys.stderr) 230 raise 231 232 def _resolve_serial(self, device, serial): 233 """Resolves the serial used for device files or generating permissions. 234 235 Returns: 236 If -s/--serial is specified, it will return that serial. 237 If -d or device: is found, it will grab the only available device. 238 If there are multiple devices, it will use $ANDROID_SERIAL. 239 Raises: 240 MissingResourceError if the resolved serial would not be usable. 241 subprocess.CalledProcessError if a command error occurs. 242 """ 243 if device: 244 if serial: 245 try: 246 output = get_output('%s -s %s get-state' % 247 (self.adb.path, serial)) 248 except subprocess.CalledProcessError: 249 raise MissingResourceError( 250 'Received error when trying to get the state of ' 251 'device with serial "%s". Is it connected and in ' 252 'device mode?' % serial) 253 if 'device' not in output: 254 raise MissingResourceError( 255 'Device "%s" is not in device mode. Reboot the phone ' 256 'into device mode and try again.' % serial) 257 return serial 258 259 elif 'ANDROID_SERIAL' in os.environ: 260 serial = os.environ['ANDROID_SERIAL'] 261 command = '%s -s %s get-state' % (self.adb, serial) 262 try: 263 output = get_output(command) 264 except subprocess.CalledProcessError: 265 raise MissingResourceError( 266 'Device with serial $ANDROID_SERIAL ("%s") not ' 267 'found.' % serial) 268 if 'device' in output: 269 return serial 270 raise MissingResourceError( 271 'Device with serial $ANDROID_SERIAL ("%s") was ' 272 'found, but was not in the "device" state.') 273 274 # Parses `adb devices` so it only returns a string of serials. 275 get_serials_cmd = ('%s devices | tail -n +2 | head -n -1 | ' 276 'cut -f1' % self.adb.path) 277 try: 278 output = get_output(get_serials_cmd) 279 # If multiple serials appear in the output, raise an error. 280 if len(output.split()) > 1: 281 raise MissingResourceError( 282 'Multiple devices are connected. You must specify ' 283 'which device to run against with flag --serial.') 284 return output.strip() 285 except subprocess.CalledProcessError: 286 print('Unexpected error when querying for connected ' 287 'devices.', file=sys.stderr) 288 raise 289 290 def _resolve_apks(self, apks): 291 """Resolves all APKs to run against. 292 293 Returns: 294 If no apk is specified in the arguments, return all apks in 295 system/priv-app. Otherwise, returns a list with the specified apk. 296 Throws: 297 MissingResourceError if the specified apk or system/priv-app cannot 298 be found. 299 """ 300 if not apks: 301 return (self._resolve_all_system_privapps(), 302 self._resolve_all_product_privapps()) 303 304 ret_apks = [] 305 for apk in apks: 306 if apk.startswith(DEVICE_PREFIX): 307 device_apk = apk[len(DEVICE_PREFIX):] 308 try: 309 apk = self.adb.pull(device_apk) 310 except subprocess.CalledProcessError: 311 raise MissingResourceError( 312 'File "%s" could not be located on device "%s".' % 313 (device_apk, self.adb.serial)) 314 ret_apks.append(apk) 315 elif not os.path.isfile(apk): 316 raise MissingResourceError('File "%s" does not exist.' % apk) 317 else: 318 ret_apks.append(apk) 319 return ret_apks, None 320 321 def _resolve_all_system_privapps(self): 322 """Extract package name and requested permissions.""" 323 if self._is_android_env: 324 system_priv_app_dir = ( 325 os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 326 'system/priv-app')) 327 else: 328 try: 329 system_priv_app_dir = self.adb.pull('/system/priv-app/') 330 except subprocess.CalledProcessError: 331 raise MissingResourceError( 332 'Directory "/system/priv-app" could not be pulled from on ' 333 'device "%s".' % self.adb.serial) 334 335 return get_output('find %s -name "*.apk"' % system_priv_app_dir).split() 336 337 def _resolve_all_product_privapps(self): 338 """Extract package name and requested permissions.""" 339 if self._is_android_env: 340 product_priv_app_dir = ( 341 os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 342 'product/priv-app')) 343 if not os.path.exists(product_priv_app_dir): 344 product_priv_app_dir = ( 345 os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 346 'system/product/priv-app')) 347 else: 348 try: 349 product_priv_app_dir = self.adb.pull('/product/priv-app/') 350 except subprocess.CalledProcessError: 351 print('Warning: Directory "/product/priv-app" could not be ' 352 'pulled from on device "%s". Trying ' 353 '"/system/product/priv-app"' % self.adb.serial, 354 file=sys.stderr) 355 try: 356 product_priv_app_dir = ( 357 self.adb.pull('/system/product/priv-app/')) 358 except subprocess.CalledProcessError: 359 raise MissingResourceError( 360 'Directory "/system/product/priv-app" could not be ' 361 'pulled from on device "%s".' % self.adb.serial) 362 363 return get_output( 364 'find %s -name "*.apk"' % product_priv_app_dir).split() 365 366 def _resolve_sys_path(self, file_path, fallback_file_path=None): 367 """Resolves a path that is a part of an Android System Image.""" 368 if self._is_android_env: 369 sys_path = ( 370 os.path.join(os.environ['ANDROID_PRODUCT_OUT'], file_path)) 371 if not os.path.exists(sys_path): 372 sys_path = ( 373 os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 374 fallback_file_path)) 375 else: 376 try: 377 sys_path = self.adb.pull(file_path) 378 except subprocess.CalledProcessError: 379 print('Warning: Directory %s could not be pulled from on device' 380 '"%s". Trying "/system/product/priv-app"' 381 % (file_path, self.adb.serial), file=sys.stderr) 382 try: 383 sys_path = self.adb.pull(fallback_file_path) 384 except subprocess.CalledProcessError: 385 raise MissingResourceError( 386 'Directory %s could not be pulled from on ' 387 'device "%s".' % (fallback_file_path, self.adb.serial)) 388 389 return sys_path 390 391 392def get_output(command): 393 """Returns the output of the command as a string. 394 395 Throws: 396 subprocess.CalledProcessError if exit status is non-zero. 397 """ 398 output = subprocess.check_output(command, shell=True) 399 # For Python3.4, decode the byte string so it is usable. 400 return output.decode(encoding='UTF-8') 401 402 403def parse_args(): 404 """Parses the CLI.""" 405 parser = argparse.ArgumentParser( 406 description=HELP_MESSAGE, 407 formatter_class=argparse.RawDescriptionHelpFormatter) 408 parser.add_argument( 409 '-d', 410 '--device', 411 action='store_true', 412 default=False, 413 required=False, 414 help='Whether or not to generate the privapp_permissions file for the ' 415 'build already on a device. See -s/--serial below for more ' 416 'details.' 417 ) 418 parser.add_argument( 419 '--adb', 420 type=str, 421 required=False, 422 metavar='<ADB_PATH', 423 help='Path to adb. If none specified, uses the environment\'s adb.' 424 ) 425 parser.add_argument( 426 '--aapt', 427 type=str, 428 required=False, 429 metavar='<AAPT_PATH>', 430 help='Path to aapt. If none specified, uses the environment\'s aapt.' 431 ) 432 parser.add_argument( 433 '-s', 434 '--serial', 435 type=str, 436 required=False, 437 metavar='<SERIAL>', 438 help='The serial of the device to generate permissions for. If no ' 439 'serial is given, it will pick the only device connected over ' 440 'adb. If multiple devices are found, it will default to ' 441 '$ANDROID_SERIAL. Otherwise, the program will exit with error ' 442 'code 1. If -s is given, -d is not needed.' 443 ) 444 parser.add_argument( 445 'apks', 446 nargs='*', 447 type=str, 448 help='A list of paths to priv-app APKs to generate permissions for. ' 449 'To make a path device-side, prefix the path with "device:".' 450 ) 451 parser.add_argument( 452 '-w', 453 '--writetodisk', 454 action='store_true', 455 default=False, 456 required=False, 457 help='Whether or not to store the generated permissions directly to ' 458 'a file. See --systemfile/--productfile for more information.' 459 ) 460 parser.add_argument( 461 '--systemfile', 462 default='./system.xml', 463 required=False, 464 help='Path to system permissions file. Default value is ./system.xml' 465 ) 466 parser.add_argument( 467 '--productfile', 468 default='./product.xml', 469 required=False, 470 help='Path to system permissions file. Default value is ./product.xml' 471 ) 472 cmd_args = parser.parse_args() 473 474 return cmd_args 475 476def create_permission_file(resources, privapp_apks, permissions_dir, 477 sysconfig_dir, file=None): 478 # Parse base XML files in /etc dir, permissions listed there don't have 479 # to be re-added 480 base_permissions = {} 481 base_xml_files = itertools.chain(list_xml_files(permissions_dir), 482 list_xml_files(sysconfig_dir)) 483 for xml_file in base_xml_files: 484 parse_config_xml(xml_file, base_permissions) 485 486 priv_permissions = extract_priv_permissions(resources.aapt, 487 resources.framework_res_apk) 488 489 apps_redefine_base = [] 490 results = {} 491 for priv_app in privapp_apks: 492 pkg_info = extract_pkg_and_requested_permissions(resources.aapt, 493 priv_app) 494 pkg_name = pkg_info['package_name'] 495 priv_perms = get_priv_permissions(pkg_info['permissions'], 496 priv_permissions) 497 # Compute diff against permissions defined in base file 498 if base_permissions and (pkg_name in base_permissions): 499 base_permissions_pkg = base_permissions[pkg_name] 500 priv_perms = remove_base_permissions(priv_perms, 501 base_permissions_pkg) 502 if priv_perms: 503 apps_redefine_base.append(pkg_name) 504 if priv_perms: 505 results[pkg_name] = sorted(priv_perms) 506 507 print_xml(results, apps_redefine_base) 508 if file is not None: 509 print_xml(results, apps_redefine_base, file) 510 511def print_xml(results, apps_redefine_base, fd=sys.stdout): 512 """Print results to the given file.""" 513 fd.write('<?xml version="1.0" encoding="utf-8"?>\n<permissions>\n') 514 for package_name in sorted(results): 515 if package_name in apps_redefine_base: 516 fd.write(' <!-- Additional permissions on top of %s -->\n' % 517 BASE_XML_FILENAME) 518 fd.write(' <privapp-permissions package="%s">\n' % package_name) 519 for p in results[package_name]: 520 fd.write(' <permission name="%s"/>\n' % p) 521 fd.write(' </privapp-permissions>\n') 522 fd.write('\n') 523 524 fd.write('</permissions>\n') 525 526 527def remove_base_permissions(priv_perms, base_perms): 528 """Removes set of base_perms from set of priv_perms.""" 529 if (not priv_perms) or (not base_perms): 530 return priv_perms 531 return set(priv_perms) - set(base_perms) 532 533 534def get_priv_permissions(requested_perms, priv_perms): 535 """Return only permissions that are in priv_perms set.""" 536 return set(requested_perms).intersection(set(priv_perms)) 537 538 539def list_xml_files(directory): 540 """Returns a list of all .xml files within a given directory. 541 542 Args: 543 directory: the directory to look for xml files in. 544 """ 545 xml_files = [] 546 for dirName, subdirList, file_list in os.walk(directory): 547 for file in file_list: 548 if file.endswith('.xml'): 549 file_path = os.path.join(dirName, file) 550 xml_files.append(file_path) 551 return xml_files 552 553 554def extract_pkg_and_requested_permissions(aapt, apk_path): 555 """ 556 Extract package name and list of requested permissions from the 557 dump of manifest file 558 """ 559 aapt_args = ['d', 'permissions', apk_path] 560 txt = aapt.call(aapt_args) 561 562 permissions = [] 563 package_name = None 564 raw_lines = txt.split('\n') 565 for line in raw_lines: 566 regex = r"uses-permission.*: name='([\S]+)'" 567 matches = re.search(regex, line) 568 if matches: 569 name = matches.group(1) 570 permissions.append(name) 571 regex = r'package: ([\S]+)' 572 matches = re.search(regex, line) 573 if matches: 574 package_name = matches.group(1) 575 576 return {'package_name': package_name, 'permissions': permissions} 577 578 579def extract_priv_permissions(aapt, apk_path): 580 """Extract signature|privileged permissions from dump of manifest file.""" 581 aapt_args = ['d', 'xmltree', apk_path, 'AndroidManifest.xml'] 582 txt = aapt.call(aapt_args) 583 raw_lines = txt.split('\n') 584 n = len(raw_lines) 585 i = 0 586 permissions_list = [] 587 while i < n: 588 line = raw_lines[i] 589 if line.find('E: permission (') != -1: 590 i += 1 591 name = None 592 level = None 593 while i < n: 594 line = raw_lines[i] 595 if line.find('E: ') != -1: 596 break 597 matches = re.search(ANDROID_NAME_REGEX, line) 598 if matches: 599 name = matches.group(1) 600 i += 1 601 continue 602 matches = re.search(ANDROID_PROTECTION_LEVEL_REGEX, line) 603 if matches: 604 level = int(matches.group(1), 16) 605 i += 1 606 continue 607 i += 1 608 if name and level and level & 0x12 == 0x12: 609 permissions_list.append(name) 610 else: 611 i += 1 612 613 return permissions_list 614 615 616def parse_config_xml(base_xml, results): 617 """Parse an XML file that will be used as base.""" 618 dom = minidom.parse(base_xml) 619 nodes = dom.getElementsByTagName('privapp-permissions') 620 for node in nodes: 621 permissions = (node.getElementsByTagName('permission') + 622 node.getElementsByTagName('deny-permission')) 623 package_name = node.getAttribute('package') 624 plist = [] 625 if package_name in results: 626 plist = results[package_name] 627 for p in permissions: 628 perm_name = p.getAttribute('name') 629 if perm_name: 630 plist.append(perm_name) 631 results[package_name] = plist 632 return results 633 634 635def cleanup(): 636 """Cleans up temp files.""" 637 for directory in temp_dirs: 638 shutil.rmtree(directory, ignore_errors=True) 639 for file in temp_files: 640 os.remove(file) 641 del temp_dirs[:] 642 del temp_files[:] 643 644 645if __name__ == '__main__': 646 args = parse_args() 647 try: 648 tool_resources = Resources( 649 aapt_path=args.aapt, 650 adb_path=args.adb, 651 use_device=args.device, 652 serial=args.serial, 653 apks=args.apks 654 ) 655 system_permission_file=None 656 product_permission_file=None 657 print('#' * 80) 658 print('#') 659 if args.writetodisk: 660 print('#System XML written to %s:' % args.systemfile) 661 system_permission_file = open(args.systemfile, 'w') 662 else: 663 print('#System XML:') 664 print('#') 665 print('#' * 80) 666 create_permission_file( 667 tool_resources, 668 tool_resources.system_privapp_apks, 669 tool_resources.system_permissions_dir, 670 tool_resources.system_sysconfig_dir, 671 system_permission_file) 672 if args.writetodisk: 673 system_permission_file.close() 674 if tool_resources.product_privapp_apks: 675 print('#' * 80) 676 print('#') 677 if args.writetodisk: 678 print('#Product XML written to %s:' % args.productfile) 679 product_permission_file = open(args.productfile, 'w') 680 else: 681 print('#Product XML:') 682 print('#') 683 print('#' * 80) 684 create_permission_file( 685 tool_resources, 686 tool_resources.product_privapp_apks, 687 tool_resources.product_permissions_dir, 688 tool_resources.product_sysconfig_dir, 689 product_permission_file) 690 if args.writetodisk: 691 product_permission_file.close() 692 except MissingResourceError as e: 693 print(str(e), file=sys.stderr) 694 exit(1) 695 finally: 696 cleanup() 697