blob: 8d97a63d9f4661772c109d12109b19653a51f13c [file] [log] [blame]
/*
* Copyright (C) 2018 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
*
* 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.
*/
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package com.android.tools.lint.checks
import com.android.SdkConstants.ANDROID_URI
import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.TAG_PERMISSION
import com.android.testutils.TestUtils
import com.android.tools.apk.analyzer.BinaryXmlParser
import com.android.tools.lint.checks.ApiLookup.get
import com.android.tools.lint.checks.infrastructure.TestLintClient
import com.android.utils.XmlUtils
import com.android.utils.XmlUtils.getFirstSubTagByName
import com.android.utils.XmlUtils.getNextTagByName
import com.google.common.base.Joiner
import com.google.common.io.ByteStreams
import org.junit.Assert.fail
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.File
import java.net.URLClassLoader
import java.util.HashMap
import kotlin.text.Charsets.UTF_8
data class Permission(
/** Permission name */
val name: String,
/** Manifest.permission class field name */
val field: String?,
/** API level this permission was introduced in */
val introducedIn: Int
) {
/** Bit mask for API levels where this permission was marked dangerous */
private var dangerous: Long = 0
/** Bit mask for API levels where this permission was marked signature/system */
private var signature: Long = 0
/** API level this permission was made a signature permission in, or 0 */
val signatureIn: Int get() = signatureFrom()
/** Last API level this permission was a signature permission in, or 0 */
val signatureOut: Int get() = signatureUntil()
/** API level this permission was made a dangerous permission in, or 0 */
val dangerousIn: Int get() = dangerousFrom()
/** Last API level this permission was a dangerous permission in, or 0 */
val dangerousOut: Int get() = dangerousUntil()
/* Mark this permission as dangerous in the given API level */
fun markDangerous(apiLevel: Int) {
dangerous = dangerous or ((1 shl apiLevel).toLong())
}
/* Mark this permission as signature/system in the given API level */
fun markSignature(apiLevel: Int) {
signature = signature or ((1 shl apiLevel).toLong())
}
/** Returns the first API level this permission is dangerous in, if any */
fun dangerousFrom(): Int = lowestBit(dangerous)
/** Returns the last API level this permission is dangerous in, if any */
fun dangerousUntil(): Int = highestBit(dangerous)
/** Returns the first API level this permission is dangerous in, if any */
fun signatureFrom(): Int = lowestBit(signature)
/** Returns the last API level this permission is dangerous in, if any */
fun signatureUntil(): Int = highestBit(signature)
override fun toString(): String {
return "Permission($name, from=$introducedIn, dangerous=${getApiLevels(dangerous)}, signature=${getApiLevels(
signature
)})"
}
private fun lowestBit(mask: Long): Int {
if (mask == 0L) {
return 0
}
var apiLevel = 0
var current = mask
while (current and 1L == 0L) {
current = current ushr 1
apiLevel++
}
return apiLevel
}
private fun highestBit(mask: Long): Int {
if (mask == 0L) {
return 0
}
var apiLevel = 0
var current = mask
var max = 0
while (current != 0L) {
if (current and 1L == 1L) {
max = apiLevel
}
current = current ushr 1
apiLevel++
}
return max
}
private fun getApiLevels(mask: Long): List<Int> {
if (mask == 0L) {
return emptyList()
}
val list = mutableListOf<Int>()
var apiLevel = 0
var current = mask
while (current != 0L) {
if (current and 1L == 1L) {
list.add(apiLevel)
}
current = current ushr 1
apiLevel++
}
return list
}
}
/**
* Analyzes the SDK to extract permission data used by various lint
* checks such as the [com.android.tools.lint.checks.SystemPermissionsDetector]
* and the [com.android.tools.lint.checks.PermissionDetector]
*/
class PermissionDataGenerator {
val permissions = computePermissions(skipHidden = false)
var maxApiLevel: Int = 0
fun getDangerousPermissions(skipHidden: Boolean = true, minApiLevel: Int): List<Permission> {
return permissions?.filter { permission ->
permission.dangerousIn > 0 &&
permission.dangerousOut >= minApiLevel &&
(!skipHidden || permission.field != null)
}?.toList() ?: emptyList()
}
fun getSignaturePermissions(skipHidden: Boolean = false): List<Permission> {
return permissions?.filter {
it.signatureIn > 0 && (!skipHidden || it.field != null)
}?.toList() ?: emptyList()
}
/**
* Returns permissions that were added as signature permissions later (not at
* the API level they were initially introduced. This only considers non-hidden
* permissions.
*/
fun getPermissionsMarkedAsSignatureLater(): List<Permission> {
permissions ?: return emptyList()
return permissions.filter { permission ->
permission.introducedIn < permission.signatureIn && permission.field != null
}.sortedWith(compareBy(Permission::signatureIn, Permission::name))
}
fun getLastNonSignatureApiLevelSwitch(): String {
// Generate exception maps
val sb = StringBuilder()
sb.append("private static int getLastNonSignatureApiLevel(@NonNull String name) {\n")
sb.append(" switch (name) {\n")
for (permission in getPermissionsMarkedAsSignatureLater()) {
// Ignore really old news
if (permission.signatureIn < 15) {
continue
}
sb.append(" case \"").append(permission.name).append("\": return ")
.append(permission.signatureIn - 1).append(";\n")
}
sb.append(" default: return -1;\n")
sb.append(" }\n")
sb.append("}\n")
return sb.toString()
}
fun getRemovedSignaturePermissions(): List<Permission> {
permissions ?: return emptyList()
return permissions.filter { permission ->
val lastSignatureApiLevel = permission.signatureOut
permission.field != null && lastSignatureApiLevel > 1 && lastSignatureApiLevel < maxApiLevel
}.sortedWith(compareBy(Permission::signatureIn, Permission::name))
}
fun getRemovedSignaturePermissionSwitch(): String {
val sb = StringBuilder()
sb.append(" switch (name) {\n")
for (permission in getRemovedSignaturePermissions()) {
// Ignore really old news
if (permission.signatureOut < 15) {
continue
}
sb.append(" case \"").append(permission.name).append("\": return ")
.append(permission.signatureOut).append(";\n")
}
sb.append(" default: return -1;\n")
sb.append(" }\n")
return sb.toString()
}
fun getPermissionsMarkedAsDangerousLater(): List<Permission> {
permissions ?: return emptyList()
return permissions.filter { permission ->
permission.introducedIn < permission.dangerousIn && permission.field != null
}.sortedWith(compareBy(Permission::dangerousIn, Permission::name))
}
fun getLastNonDangerousApiLevelSwitch(): String {
// Generate exception maps
val sb = StringBuilder()
sb.append("private static int getLastNonDangerousApiLevel(@NonNull String name) {\n")
sb.append(" switch (name) {\n")
for (permission in getPermissionsMarkedAsDangerousLater()) {
// Ignore really old news
if (permission.dangerousIn < 15) {
continue
}
sb.append(" case \"").append(permission.name).append("\": return ")
.append(permission.dangerousIn - 1).append(";\n")
}
sb.append(" default: return -1;\n")
sb.append(" }\n")
sb.append("}\n")
return sb.toString()
}
fun getRemovedDangerousPermissions(): List<Permission> {
permissions ?: return emptyList()
return permissions.filter { permission ->
val lastDangerousApiLevel = permission.dangerousOut
permission.field != null && lastDangerousApiLevel > 1 && lastDangerousApiLevel < maxApiLevel
}.sortedWith(compareBy(Permission::dangerousOut, Permission::name))
}
fun getRemovedDangerousPermissionsSwitch(): String {
val sb = StringBuilder()
sb.append(" switch (name) {\n")
for (permission in getRemovedDangerousPermissions()) {
// Ignore really old news
if (permission.dangerousIn < 15) {
continue
}
sb.append(" case \"").append(permission.name).append("\": return ")
.append(permission.dangerousOut).append(";\n")
}
sb.append(" default: return -1;\n")
sb.append(" }\n")
return sb.toString()
}
fun getPermissionNames(permissions: List<Permission>): Array<String> {
return permissions.map { it.name }.sortedBy { it }.toTypedArray()
}
fun assertSamePermissions(expected: Array<String>, actual: Array<String>) {
if (!actual.contentEquals(expected)) {
println("Correct list of permissions:")
for (name in actual) {
println(" \"$name\",")
}
fail(
"List of revocable permissions has changed:\n" +
// Make the diff show what it take to bring the actual results into the
// expected results
TestUtils.getDiff(
Joiner.on('\n').join(expected),
Joiner.on('\n').join(actual)
)
)
}
}
private fun computePermissions(skipHidden: Boolean = false): List<Permission>? {
val top = System.getenv("ANDROID_BUILD_TOP") ?: return null
val apiLookup = getApiLookup() ?: return null
val nameToPermission = HashMap<String, Permission>()
var apiLevel = 1
while (true) {
val jar = findSdkJar(top, apiLevel) ?: break
val loader = URLClassLoader(arrayOf(jar.toURI().toURL()))
val valueToFieldName = computeFieldToPermissionNameMap(loader)
val document = getManifestDocument(loader)
if (document != null) {
var element = getFirstSubTagByName(document.documentElement, TAG_PERMISSION)
while (element != null) {
processPermissionTag(
element,
apiLevel,
nameToPermission,
valueToFieldName,
skipHidden,
apiLookup
)
element = getNextTagByName(element, TAG_PERMISSION)
}
}
apiLevel++
}
maxApiLevel = apiLevel - 1
return nameToPermission.values.sortedBy { it.name }.toList()
}
private fun isDangerousPermission(
protectionLevels: List<String>,
name: String,
apiLevel: Int
): Boolean {
if (apiLevel >= 23 && name == "android.permission.GET_ACCOUNTS") {
// No longer needed in M. See issue 223244.
return false
}
return protectionLevels.contains("dangerous")
}
private fun isSignaturePermission(protectionLevels: List<String>): Boolean =
protectionLevels.contains("signature") ||
protectionLevels.contains("privileged") ||
protectionLevels.contains("signatureOrSystem")
private fun processPermissionTag(
element: Element,
apiLevel: Int,
nameToPermission: MutableMap<String, Permission>,
valueToFieldName: Map<String, String>,
skipHidden: Boolean,
apiLookup: ApiLookup
) {
val name = element.getAttributeNS(ANDROID_URI, ATTR_NAME)
if (name.isEmpty()) {
return
}
val field = valueToFieldName[name]
if (skipHidden && field == null) {
return
}
val protectionLevels = getProtectionLevels(element)
val signaturePermission = isSignaturePermission(protectionLevels)
val dangerousPermission = isDangerousPermission(protectionLevels, name, apiLevel)
if (dangerousPermission || signaturePermission) {
val permission = nameToPermission[name] ?: run {
val fieldVersion = if (field != null) apiLookup.getFieldVersion(
"android/Manifest\$permission", field
) else -1
val new = Permission(name, field, fieldVersion)
nameToPermission.put(name, new)
new
}
if (signaturePermission) {
permission.markSignature(apiLevel)
}
if (dangerousPermission) {
permission.markDangerous(apiLevel)
}
}
}
private fun getProtectionLevels(element: Element): List<String> {
val attribute = element.getAttributeNS(ANDROID_URI, "protectionLevel")
val protectionLevel = if (attribute.isEmpty()) {
"0"
} else {
attribute
}
if (Character.isDigit(protectionLevel[0])) {
val protectionLevels = mutableListOf<String>()
val protectionLevelInt = Integer.decode(protectionLevel)
// See res/values/attrs_manifest.xml for flag declarations
if (flagSet(protectionLevelInt, 2)) {
protectionLevels.add("signature")
}
if (flagSet(protectionLevelInt, 3)) {
protectionLevels.add("signatureOrSystem")
} else if (flagSet(protectionLevelInt, 1)) {
protectionLevels.add("dangerous")
}
if (flagSet(protectionLevelInt, 0x10)) {
protectionLevels.add("privileged")
}
if (flagSet(protectionLevelInt, 0x20)) {
protectionLevels.add("development")
}
if (flagSet(protectionLevelInt, 0x40)) {
protectionLevels.add("appop")
}
if (flagSet(protectionLevelInt, 0x80)) {
protectionLevels.add("pre23")
}
if (flagSet(protectionLevelInt, 0x100)) {
protectionLevels.add("verifier")
}
if (flagSet(protectionLevelInt, 0x400)) {
protectionLevels.add("preinstalled")
}
if (flagSet(protectionLevelInt, 0x800)) {
protectionLevels.add("setup")
}
if (flagSet(protectionLevelInt, 0x1000)) {
protectionLevels.add("instant")
}
if (flagSet(protectionLevelInt, 0x2000)) {
protectionLevels.add("runtime")
}
return protectionLevels
} else {
// Already a string (probably parsing source level XML)
return protectionLevel.split("\\|").toList()
}
}
private fun flagSet(protectionLevelInt: Int, flag: Int): Boolean {
return protectionLevelInt and flag == flag
}
/** Returns the android.jar file for the given API level, or null if not found/valid */
private fun findSdkJar(top: String, apiLevel: Int): File? {
var jar = File(top, "prebuilts/sdk/$apiLevel/current/android.jar")
if (!jar.exists()) {
jar = File(
top, // API levels 1, 2 and 3
"prebuilts/tools/common/api-versions/android-$apiLevel/android.jar"
)
if (!jar.exists()) {
if (apiLevel < 25) {
System.err.println("Expected to find all the jar files here")
}
return null
}
}
return jar
}
private fun getApiLookup(): ApiLookup? {
return get(object : TestLintClient() {
override fun getSdkHome(): File? {
return TestUtils.getSdk()
}
}) ?: run {
println("Couldn't find API database")
return null
}
}
/**
* Finds the binary AndroidManifest.xml in the given compiled SDK jar and
* returns the DOM document of a pretty printed version of it
*/
private fun getManifestDocument(loader: URLClassLoader): Document? {
val stream = loader.getResourceAsStream("AndroidManifest.xml")
val bytes = ByteStreams.toByteArray(stream)
stream.close()
val xml = String(BinaryXmlParser.decodeXml("AndroidManifest.xml", bytes), UTF_8)
return XmlUtils.parseDocumentSilently(xml, true)
}
/** Computes map from permission name to field in Manifest.permission */
private fun computeFieldToPermissionNameMap(loader: URLClassLoader): HashMap<String, String> {
val clz = Class.forName("android.Manifest\$permission", true, loader)
val valueToFieldName = HashMap<String, String>()
for (field in clz.declaredFields) {
val initial = field.get(null)
if (initial is String) {
val fieldName = field.name
valueToFieldName.put(initial, fieldName)
}
}
return valueToFieldName
}
}