verify-permissions.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/python3
  2. #
  3. # Copyright (C) 2021 Paul Keith <[email protected]>
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License version 2 and
  7. # only version 2 as published by the Free Software Foundation.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. import errno
  14. from glob import glob
  15. import os
  16. import subprocess
  17. import sys
  18. from xml.etree import ElementTree
  19. # Get external packages
  20. try:
  21. from parse import parse
  22. except ImportError:
  23. print('Please install the "parse" package via pip3.')
  24. exit(errno.ENOPKG)
  25. try:
  26. import requests
  27. except ImportError:
  28. print('Please install the "requests" package via pip3.')
  29. exit(errno.ENOPKG)
  30. # Change working directory to the location of this script
  31. # This fixes relative path references when calling this script from
  32. # outside of the directory containing it
  33. os.chdir(sys.path[0])
  34. # Definitions for privileged permissions
  35. ANDROID_MANIFEST_XML = \
  36. 'https://raw.githubusercontent.com/LineageOS/android_frameworks_base/lineage-19.0/core/res/AndroidManifest.xml'
  37. ANDROID_XML_NS = '{http://schemas.android.com/apk/res/android}'
  38. privileged_permissions = set()
  39. privileged_permission_mask = {'privileged', 'signature'}
  40. # Get AndroidManifest.xml
  41. req = requests.get(ANDROID_MANIFEST_XML)
  42. # Parse AndroidManifest.xml to get signature|privileged permissions
  43. root = ElementTree.fromstring(req.text)
  44. for perm in root.findall('permission'):
  45. # Get name of permission
  46. name = perm.get(f'{ANDROID_XML_NS}name')
  47. # Get the protection levels on the permission
  48. levels = set(
  49. perm.get(f'{ANDROID_XML_NS}protectionLevel').split('|'))
  50. # Check if the protections include signature and privileged
  51. levels_masked = levels & privileged_permission_mask
  52. if len(levels_masked) == len(privileged_permission_mask):
  53. privileged_permissions.add(name)
  54. # Definitions for privapp-permissions
  55. # Dictionary with structure:
  56. # package_name : (set(allowed_permissions), set(requested_permissions))
  57. privapp_permissions_dict = {}
  58. # Definitions for privapp-permission allowlists
  59. GLOB_XML_STR = '../*/proprietary/*/etc/permissions/privapp-permissions*.xml'
  60. # Parse allowlists to extract allowed privileged permissions
  61. for allowlist in glob(GLOB_XML_STR):
  62. # Get root of XML
  63. tree = ElementTree.parse(allowlist)
  64. root = tree.getroot()
  65. # Loop through and find packages
  66. for package in root.findall('privapp-permissions'):
  67. name = package.get('package')
  68. # Create empty entry if it's not in the dictionary
  69. if name not in privapp_permissions_dict:
  70. privapp_permissions_dict[name] = (set(), set())
  71. # Get all permissions and add them to dictionary
  72. for permission in package.findall('permission'):
  73. privapp_permissions_dict[name][0].add(permission.get('name'))
  74. for permission in package.findall('deny-permission'):
  75. privapp_permissions_dict[name][0].add(permission.get('name'))
  76. # Definitions for parsing APKs
  77. GLOB_APK_STR = '../*/proprietary/*/priv-app/*/*.apk'
  78. AAPT_CMD = ['aapt', 'd', 'permissions']
  79. # Extract requested privileged permissions from all priv-app APKs
  80. for apk in glob(GLOB_APK_STR):
  81. # Run 'aapt d permissions' on APK
  82. aapt_output = subprocess.check_output(AAPT_CMD + [apk],
  83. stderr=subprocess.STDOUT).decode(encoding='UTF-8')
  84. lines = aapt_output.splitlines()
  85. # Extract package name from the output
  86. # Output looks like:
  87. # package: my.package.name
  88. package_name = parse('package: {}', lines[0])[0]
  89. # Create empty entry if package is not in dic
  90. if package_name not in privapp_permissions_dict:
  91. privapp_permissions_dict[package_name] = (set(), set())
  92. # Extract 'uses-permission' lines from the rest of the output
  93. # Relevant output looks like:
  94. # uses-permission: name='permission'
  95. for line in lines[1:]:
  96. # Extract permission name and add it to the dictionary if it's
  97. # one of the privileged permissions we extracted earlier
  98. if perm_name := parse('uses-permission: name=\'{}\'', line):
  99. if perm_name[0] in privileged_permissions:
  100. privapp_permissions_dict[package_name][1].add(perm_name[0])
  101. # Keep track of exit code
  102. rc = 0
  103. # Loop through all the packages and compare permission sets
  104. for package in privapp_permissions_dict:
  105. # Get the sets of permissions
  106. # Format is (allowed, requested)
  107. perm_sets = privapp_permissions_dict[package]
  108. # Compute the set difference requested - allowed
  109. # This gives us all the permissions requested that were not allowed
  110. perm_diff = perm_sets[1] - perm_sets[0]
  111. # If any permissions are left, set exit code to EPERM and print output
  112. if len(perm_diff) > 0:
  113. rc = errno.EPERM
  114. sys.stderr.write(f"Package {package} is missing these permissions:\n")
  115. for perm in perm_diff:
  116. sys.stderr.write(f" - {perm}\n")
  117. # Exit program
  118. exit(rc)