1 /*
2  * Copyright (C) 2022 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.android.lint.aidl
18 
19 import com.android.tools.lint.client.api.UElementHandler
20 import com.android.tools.lint.detector.api.AnnotationInfo
21 import com.android.tools.lint.detector.api.AnnotationOrigin
22 import com.android.tools.lint.detector.api.AnnotationUsageInfo
23 import com.android.tools.lint.detector.api.AnnotationUsageType
24 import com.android.tools.lint.detector.api.Category
25 import com.android.tools.lint.detector.api.ConstantEvaluator
26 import com.android.tools.lint.detector.api.Detector
27 import com.android.tools.lint.detector.api.Implementation
28 import com.android.tools.lint.detector.api.Issue
29 import com.android.tools.lint.detector.api.JavaContext
30 import com.android.tools.lint.detector.api.Scope
31 import com.android.tools.lint.detector.api.Severity
32 import com.android.tools.lint.detector.api.SourceCodeScanner
33 import com.google.android.lint.findCallExpression
34 import com.intellij.psi.PsiAnnotation
35 import com.intellij.psi.PsiArrayInitializerMemberValue
36 import com.intellij.psi.PsiClass
37 import com.intellij.psi.PsiElement
38 import com.intellij.psi.PsiMethod
39 import org.jetbrains.uast.UBlockExpression
40 import org.jetbrains.uast.UDeclarationsExpression
41 import org.jetbrains.uast.UElement
42 import org.jetbrains.uast.UExpression
43 import org.jetbrains.uast.UMethod
44 import org.jetbrains.uast.skipParenthesizedExprDown
45 
46 import java.util.EnumSet
47 
48 /**
49  * Lint Detector that ensures consistency when using the @EnforcePermission
50  * annotation. Multiple verifications are implemented:
51  *
52  *  1. Visit any annotation usage, to ensure that any derived class will have
53  *     the correct annotation on each methods. Even if the subclass does not
54  *     have the annotation, visitAnnotationUsage will be called which allows us
55  *     to capture the issue.
56  *  2. Visit any method, to ensure that if a method is annotated, it has
57  *     its ancestor also annotated. This is to avoid having an annotation on a
58  *     Java method without the corresponding annotation on the AIDL interface.
59  *  3. When annotated, ensures that the first instruction is to call the helper
60  *     method (or the parent helper).
61  */
62 class EnforcePermissionDetector : Detector(), SourceCodeScanner {
63 
applicableAnnotationsnull64     override fun applicableAnnotations(): List<String> {
65         return listOf(ANNOTATION_ENFORCE_PERMISSION)
66     }
67 
getApplicableUastTypesnull68     override fun getApplicableUastTypes(): List<Class<out UElement?>> =
69             listOf(UMethod::class.java)
70 
71     private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> {
72         if (elem is PsiArrayInitializerMemberValue)
73             return elem.getInitializers().map { it as PsiElement }.toTypedArray()
74         return elem.getChildren()
75     }
76 
areAnnotationsEquivalentnull77     private fun areAnnotationsEquivalent(
78         context: JavaContext,
79         anno1: PsiAnnotation,
80         anno2: PsiAnnotation
81     ): Boolean {
82         if (anno1.qualifiedName != anno2.qualifiedName) {
83             return false
84         }
85         val attr1 = anno1.parameterList.attributes
86         val attr2 = anno2.parameterList.attributes
87         if (attr1.size != attr2.size) {
88             return false
89         }
90         for (i in attr1.indices) {
91             if (attr1[i].name != attr2[i].name) {
92                 return false
93             }
94             val value1 = attr1[i].value ?: return false
95             val value2 = attr2[i].value ?: return false
96             // Try to compare values directly with each other.
97             val v1 = ConstantEvaluator.evaluate(context, value1)
98             val v2 = ConstantEvaluator.evaluate(context, value2)
99             if (v1 != null && v2 != null) {
100                 if (v1 != v2 && !isOneShortPermissionOfOther(v1, v2)) {
101                     return false
102                 }
103             } else {
104                 val children1 = annotationValueGetChildren(value1)
105                 val children2 = annotationValueGetChildren(value2)
106                 if (children1.size != children2.size) {
107                     return false
108                 }
109                 for (j in children1.indices) {
110                     val c1 = ConstantEvaluator.evaluate(context, children1[j])
111                     val c2 = ConstantEvaluator.evaluate(context, children2[j])
112                     if (c1 != c2 && !isOneShortPermissionOfOther(c1, c2)) {
113                         return false
114                     }
115                 }
116             }
117         }
118         return true
119     }
120 
isOneShortPermissionOfOthernull121     private fun isOneShortPermissionOfOther(
122         permission1: Any?,
123         permission2: Any?
124     ): Boolean = permission1 == (permission2 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL) ||
125             permission2 == (permission1 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL)
126 
127     private fun compareMethods(
128         context: JavaContext,
129         element: UElement,
130         overridingMethod: PsiMethod,
131         overriddenMethod: PsiMethod,
132         checkEquivalence: Boolean = true
133     ) {
134         val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
135         val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
136         val location = context.getLocation(element)
137         val overridingClass = overridingMethod.parent as PsiClass
138         val overriddenClass = overriddenMethod.parent as PsiClass
139         val overridingName = "${overridingClass.name}.${overridingMethod.name}"
140         val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}"
141         if (overridingAnnotation == null) {
142             val msg = "The method $overridingName overrides the method $overriddenName which " +
143                 "is annotated with @EnforcePermission. The same annotation must be used " +
144                 "on $overridingName"
145             context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
146         } else if (overriddenAnnotation == null) {
147             val msg = "The method $overridingName overrides the method $overriddenName which " +
148                 "is not annotated with @EnforcePermission. The same annotation must be " +
149                 "used on $overriddenName. Did you forget to annotate the AIDL definition?"
150             context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
151         } else if (checkEquivalence && !areAnnotationsEquivalent(
152                     context, overridingAnnotation, overriddenAnnotation)) {
153             val msg = "The method $overridingName is annotated with " +
154                 "${overridingAnnotation.text} which differs from the overridden " +
155                 "method $overriddenName: ${overriddenAnnotation.text}. The same " +
156                 "annotation must be used for both methods."
157             context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
158         }
159     }
160 
visitAnnotationUsagenull161     override fun visitAnnotationUsage(
162         context: JavaContext,
163         element: UElement,
164         annotationInfo: AnnotationInfo,
165         usageInfo: AnnotationUsageInfo
166     ) {
167         if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
168             annotationInfo.origin == AnnotationOrigin.METHOD) {
169             /* Ignore implementations that are not a sub-class of Stub (i.e., Proxy). */
170             val uMethod = element as? UMethod ?: return
171             if (getContainingAidlInterface(context, uMethod) == null) {
172                 return
173             }
174             val overridingMethod = element.sourcePsi as PsiMethod
175             val overriddenMethod = usageInfo.referenced as PsiMethod
176             compareMethods(context, element, overridingMethod, overriddenMethod)
177         }
178     }
179 
createUastHandlernull180     override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
181 
182     private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
183         override fun visitMethod(node: UMethod) {
184             if (context.evaluator.isAbstract(node)) return
185             if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return
186 
187             val stubClass = containingStub(context, node)
188             if (stubClass == null) {
189                 context.report(
190                     ISSUE_MISUSING_ENFORCE_PERMISSION,
191                     node,
192                     context.getLocation(node),
193                     "The class of ${node.name} does not inherit from an AIDL generated Stub class"
194                 )
195                 return
196             }
197 
198             /* Check that we are connected to the super class */
199             val overridingMethod = node as PsiMethod
200             val parents = overridingMethod.findSuperMethods(stubClass)
201             if (parents.isEmpty()) {
202                 context.report(
203                     ISSUE_MISUSING_ENFORCE_PERMISSION,
204                     node,
205                     context.getLocation(node),
206                     "The method ${node.name} does not override an AIDL generated method"
207                 )
208                 return
209             }
210             for (overriddenMethod in parents) {
211                 // The equivalence check can be skipped, if both methods are
212                 // annotated, it will be verified by visitAnnotationUsage.
213                 compareMethods(context, node, overridingMethod,
214                     overriddenMethod, checkEquivalence = false)
215             }
216 
217             /* Check that the helper is called as a first instruction */
218             val targetExpression = getHelperMethodCallSourceString(node)
219             val message =
220                 "Method must start with $targetExpression or super.${node.name}(), if applicable"
221 
222             val firstExpression = (node.uastBody as? UBlockExpression)
223                     ?.expressions?.firstOrNull()
224 
225             if (firstExpression == null) {
226                 context.report(
227                     ISSUE_ENFORCE_PERMISSION_HELPER,
228                     context.getLocation(node),
229                     message,
230                 )
231                 return
232             }
233 
234             val firstExpressionSource = firstExpression.skipParenthesizedExprDown()
235               .asSourceString()
236               .filterNot(Char::isWhitespace)
237 
238             if (firstExpressionSource != targetExpression &&
239                   firstExpressionSource != "super.$targetExpression") {
240                 // calling super.<methodName>() is also legal
241                 val directSuper = context.evaluator.getSuperMethod(node)
242                 val firstCall = findCallExpression(firstExpression)?.resolve()
243                 if (directSuper != null && firstCall == directSuper) return
244 
245                 val locationTarget = getLocationTarget(firstExpression)
246                 val expressionLocation = context.getLocation(locationTarget)
247 
248                 context.report(
249                     ISSUE_ENFORCE_PERMISSION_HELPER,
250                     context.getLocation(node),
251                     message,
252                     getHelperMethodFix(node, expressionLocation),
253                 )
254             }
255         }
256     }
257 
258     companion object {
259 
260         private const val HELPER_SUFFIX = "_enforcePermission"
261 
262         val EXPLANATION = """
263             The @EnforcePermission annotation is used to delegate the verification of the caller's
264             permissions to a generated AIDL method.
265 
266             In order to surface that information to platform developers, the same annotation must be
267             used on the implementation class or methods.
268 
269             The @EnforcePermission annotation can only be used on methods whose class extends from
270             the Stub class generated by the AIDL compiler. When @EnforcePermission is applied, the
271             AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX.
272 
273             yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can
274             either call it directly, or call it indirectly via super.yourMethodName().
275             """
276 
277         val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create(
278             id = "MissingEnforcePermissionAnnotation",
279             briefDescription = "Missing @EnforcePermission annotation on Binder method",
280             explanation = EXPLANATION,
281             category = Category.SECURITY,
282             priority = 6,
283             severity = Severity.ERROR,
284             implementation = Implementation(
285                     EnforcePermissionDetector::class.java,
286                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
287             )
288         )
289 
290         val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create(
291             id = "MismatchingEnforcePermissionAnnotation",
292             briefDescription = "Incorrect @EnforcePermission annotation on Binder method",
293             explanation = EXPLANATION,
294             category = Category.SECURITY,
295             priority = 6,
296             severity = Severity.ERROR,
297             implementation = Implementation(
298                     EnforcePermissionDetector::class.java,
299                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
300             )
301         )
302 
303         val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create(
304             id = "MissingEnforcePermissionHelper",
305             briefDescription = """Missing permission-enforcing method call in AIDL method
306                 |annotated with @EnforcePermission""".trimMargin(),
307             explanation = EXPLANATION,
308             category = Category.SECURITY,
309             priority = 6,
310             severity = Severity.ERROR,
311             implementation = Implementation(
312                     EnforcePermissionDetector::class.java,
313                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
314             )
315         )
316 
317         val ISSUE_MISUSING_ENFORCE_PERMISSION: Issue = Issue.create(
318             id = "MisusingEnforcePermissionAnnotation",
319             briefDescription = "@EnforcePermission cannot be used here",
320             explanation = EXPLANATION,
321             category = Category.SECURITY,
322             priority = 6,
323             severity = Severity.ERROR,
324             implementation = Implementation(
325                     EnforcePermissionDetector::class.java,
326                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
327             )
328         )
329 
330         /**
331          * handles an edge case with UDeclarationsExpression, where sourcePsi is null,
332          * resulting in an incorrect Location if used directly
333          */
getLocationTargetnull334         private fun getLocationTarget(firstExpression: UExpression): PsiElement? {
335             if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi
336             if (firstExpression is UDeclarationsExpression) {
337                 return firstExpression.declarations.firstOrNull()?.sourcePsi
338             }
339             return null
340         }
341     }
342 }
343