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