1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.errorprone.bugpatterns.android; 18 19 import static com.google.errorprone.BugPattern.LinkType.NONE; 20 import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; 21 import static com.google.errorprone.bugpatterns.android.RequiresPermissionChecker.simpleNameMatches; 22 import static com.google.errorprone.matchers.Matchers.allOf; 23 import static com.google.errorprone.matchers.Matchers.anyOf; 24 import static com.google.errorprone.matchers.Matchers.enclosingClass; 25 import static com.google.errorprone.matchers.Matchers.isStatic; 26 import static com.google.errorprone.matchers.Matchers.isSubtypeOf; 27 import static com.google.errorprone.matchers.Matchers.methodHasVisibility; 28 import static com.google.errorprone.matchers.Matchers.methodIsConstructor; 29 import static com.google.errorprone.matchers.Matchers.methodIsNamed; 30 import static com.google.errorprone.matchers.Matchers.not; 31 import static com.google.errorprone.matchers.Matchers.packageStartsWith; 32 33 import android.annotation.RequiresNoPermission; 34 import android.annotation.RequiresPermission; 35 import android.annotation.SuppressLint; 36 37 import com.google.auto.service.AutoService; 38 import com.google.errorprone.BugPattern; 39 import com.google.errorprone.VisitorState; 40 import com.google.errorprone.bugpatterns.BugChecker; 41 import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; 42 import com.google.errorprone.matchers.Description; 43 import com.google.errorprone.matchers.Matcher; 44 import com.google.errorprone.matchers.MethodVisibility.Visibility; 45 import com.google.errorprone.util.ASTHelpers; 46 import com.sun.source.tree.ClassTree; 47 import com.sun.source.tree.MethodTree; 48 import com.sun.source.tree.Tree; 49 import com.sun.source.util.TreePath; 50 import com.sun.tools.javac.code.Symbol; 51 import com.sun.tools.javac.code.Symbol.MethodSymbol; 52 53 import java.util.Arrays; 54 import java.util.Collections; 55 import java.util.regex.Pattern; 56 57 /** 58 * Verifies that all Bluetooth APIs have consistent permissions. 59 */ 60 @AutoService(BugChecker.class) 61 @BugPattern( 62 name = "AndroidFrameworkBluetoothPermission", 63 summary = "Verifies that all Bluetooth APIs have consistent permissions", 64 linkType = NONE, 65 severity = WARNING) 66 public final class BluetoothPermissionChecker extends BugChecker implements MethodTreeMatcher { 67 private static final Matcher<MethodTree> BLUETOOTH_API = allOf( 68 packageStartsWith("android.bluetooth"), 69 methodHasVisibility(Visibility.PUBLIC), 70 not(isStatic()), 71 not(methodIsConstructor()), 72 not(enclosingClass(isInsideParcelable())), 73 not(enclosingClass(simpleNameMatches(Pattern.compile(".+Callback$")))), 74 not(enclosingClass(isSubtypeOf("android.bluetooth.BluetoothProfileConnector"))), 75 not(enclosingClass(isSubtypeOf("android.app.PropertyInvalidatedCache")))); 76 77 private static final Matcher<ClassTree> PARCELABLE_CLASS = 78 isSubtypeOf("android.os.Parcelable"); 79 private static final Matcher<MethodTree> BINDER_METHOD = enclosingClass( 80 isSubtypeOf("android.os.IInterface")); 81 82 private static final Matcher<MethodTree> BINDER_INTERNALS = allOf( 83 enclosingClass(isSubtypeOf("android.os.IInterface")), 84 anyOf( 85 methodIsNamed("onTransact"), 86 methodIsNamed("dump"), 87 enclosingClass(simpleNameMatches(Pattern.compile("^(Stub|Default|Proxy)$"))))); 88 89 private static final Matcher<MethodTree> GENERIC_INTERNALS = anyOf( 90 methodIsNamed("close"), 91 methodIsNamed("finalize"), 92 methodIsNamed("equals"), 93 methodIsNamed("hashCode"), 94 methodIsNamed("toString")); 95 96 private static final String PERMISSION_ADVERTISE = "android.permission.BLUETOOTH_ADVERTISE"; 97 private static final String PERMISSION_CONNECT = "android.permission.BLUETOOTH_CONNECT"; 98 private static final String PERMISSION_SCAN = "android.permission.BLUETOOTH_SCAN"; 99 100 private static final String ANNOTATION_ADVERTISE = 101 "android.bluetooth.annotations.RequiresBluetoothAdvertisePermission"; 102 private static final String ANNOTATION_CONNECT = 103 "android.bluetooth.annotations.RequiresBluetoothConnectPermission"; 104 private static final String ANNOTATION_SCAN = 105 "android.bluetooth.annotations.RequiresBluetoothScanPermission"; 106 107 @Override matchMethod(MethodTree tree, VisitorState state)108 public Description matchMethod(MethodTree tree, VisitorState state) { 109 // Ignore methods outside Bluetooth area 110 if (!BLUETOOTH_API.matches(tree, state)) return Description.NO_MATCH; 111 112 // Ignore certain types of generated or internal code 113 if (BINDER_INTERNALS.matches(tree, state)) return Description.NO_MATCH; 114 if (GENERIC_INTERNALS.matches(tree, state)) return Description.NO_MATCH; 115 116 // Skip abstract methods, except for binder interfaces 117 if (tree.getBody() == null && !BINDER_METHOD.matches(tree, state)) { 118 return Description.NO_MATCH; 119 } 120 121 // Ignore callbacks which don't need permission enforcement 122 final MethodSymbol symbol = ASTHelpers.getSymbol(tree); 123 if (isCallbackOrWrapper(symbol)) return Description.NO_MATCH; 124 125 // Ignore when suppressed 126 if (isSuppressed(symbol)) return Description.NO_MATCH; 127 128 final RequiresPermission requiresPerm = ASTHelpers.getAnnotation(tree, 129 RequiresPermission.class); 130 final RequiresNoPermission requiresNoPerm = ASTHelpers.getAnnotation(tree, 131 RequiresNoPermission.class); 132 133 final boolean requiresValid = requiresPerm != null 134 && (requiresPerm.value() != null || requiresPerm.allOf() != null); 135 final boolean requiresNoValid = requiresNoPerm != null; 136 if (!requiresValid && !requiresNoValid) { 137 return buildDescription(tree) 138 .setMessage("Method " + symbol.name.toString() 139 + "() must be protected by at least one permission") 140 .build(); 141 } 142 143 // No additional checks needed for Binder generated code 144 if (BINDER_METHOD.matches(tree, state)) return Description.NO_MATCH; 145 146 if (ASTHelpers.hasAnnotation(tree, ANNOTATION_ADVERTISE, 147 state) != isPermissionReferenced(requiresPerm, PERMISSION_ADVERTISE)) { 148 return buildDescription(tree) 149 .setMessage("Method " + symbol.name.toString() 150 + "() has inconsistent annotations for " + PERMISSION_ADVERTISE) 151 .build(); 152 } 153 if (ASTHelpers.hasAnnotation(tree, ANNOTATION_CONNECT, 154 state) != isPermissionReferenced(requiresPerm, PERMISSION_CONNECT)) { 155 return buildDescription(tree) 156 .setMessage("Method " + symbol.name.toString() 157 + "() has inconsistent annotations for " + PERMISSION_CONNECT) 158 .build(); 159 } 160 if (ASTHelpers.hasAnnotation(tree, ANNOTATION_SCAN, 161 state) != isPermissionReferenced(requiresPerm, PERMISSION_SCAN)) { 162 return buildDescription(tree) 163 .setMessage("Method " + symbol.name.toString() 164 + "() has inconsistent annotations for " + PERMISSION_SCAN) 165 .build(); 166 } 167 168 return Description.NO_MATCH; 169 } 170 isPermissionReferenced(RequiresPermission anno, String perm)171 private static boolean isPermissionReferenced(RequiresPermission anno, String perm) { 172 if (anno == null) return false; 173 if (perm.equals(anno.value())) return true; 174 return anno.allOf() != null && Arrays.asList(anno.allOf()).contains(perm); 175 } 176 isCallbackOrWrapper(Symbol symbol)177 private static boolean isCallbackOrWrapper(Symbol symbol) { 178 if (symbol == null) return false; 179 final String name = symbol.name.toString(); 180 return isCallbackOrWrapper(ASTHelpers.enclosingClass(symbol)) 181 || name.endsWith("Callback") 182 || name.endsWith("Wrapper"); 183 } 184 isSuppressed(Symbol symbol)185 public boolean isSuppressed(Symbol symbol) { 186 if (symbol == null) return false; 187 return isSuppressed(ASTHelpers.enclosingClass(symbol)) 188 || isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressWarnings.class)) 189 || isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressLint.class)); 190 } 191 isSuppressed(SuppressWarnings anno)192 private boolean isSuppressed(SuppressWarnings anno) { 193 return (anno != null) && !Collections.disjoint(Arrays.asList(anno.value()), allNames()); 194 } 195 isSuppressed(SuppressLint anno)196 private boolean isSuppressed(SuppressLint anno) { 197 return (anno != null) && !Collections.disjoint(Arrays.asList(anno.value()), allNames()); 198 } 199 isInsideParcelable()200 private static Matcher<ClassTree> isInsideParcelable() { 201 return new Matcher<ClassTree>() { 202 @Override 203 public boolean matches(ClassTree tree, VisitorState state) { 204 final TreePath path = state.getPath(); 205 for (Tree node : path) { 206 if (node instanceof ClassTree 207 && PARCELABLE_CLASS.matches((ClassTree) node, state)) { 208 return true; 209 } 210 } 211 return false; 212 } 213 }; 214 } 215 } 216