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