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.intellij.psi.PsiAnnotation
34 import com.intellij.psi.PsiArrayInitializerMemberValue
35 import com.intellij.psi.PsiClass
36 import com.intellij.psi.PsiElement
37 import com.intellij.psi.PsiMethod
38 import org.jetbrains.uast.UAnnotation
39 import org.jetbrains.uast.UElement
40 import org.jetbrains.uast.UMethod
41 import org.jetbrains.uast.toUElement
42 
43 /**
44  * Lint Detector that ensures that any method overriding a method annotated
45  * with @EnforcePermission is also annotated with the exact same annotation.
46  * The intent is to surface the effective permission checks to the service
47  * implementations.
48  *
49  * This is done with 2 mechanisms:
50  *  1. Visit any annotation usage, to ensure that any derived class will have
51  *     the correct annotation on each methods. This is for the top to bottom
52  *     propagation.
53  *  2. Visit any annotation, to ensure that if a method is annotated, it has
54  *     its ancestor also annotated. This is to avoid having an annotation on a
55  *     Java method without the corresponding annotation on the AIDL interface.
56  */
57 class EnforcePermissionDetector : Detector(), SourceCodeScanner {
58 
59     override fun applicableAnnotations(): List<String> {
60         return listOf(ANNOTATION_ENFORCE_PERMISSION)
61     }
62 
63     override fun getApplicableUastTypes(): List<Class<out UElement>> {
64         return listOf(UAnnotation::class.java)
65     }
66 
67     private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> {
68         if (elem is PsiArrayInitializerMemberValue)
69             return elem.getInitializers().map { it as PsiElement }.toTypedArray()
70         return elem.getChildren()
71     }
72 
73     private fun areAnnotationsEquivalent(
74         context: JavaContext,
75         anno1: PsiAnnotation,
76         anno2: PsiAnnotation
77     ): Boolean {
78         if (anno1.qualifiedName != anno2.qualifiedName) {
79             return false
80         }
81         val attr1 = anno1.parameterList.attributes
82         val attr2 = anno2.parameterList.attributes
83         if (attr1.size != attr2.size) {
84             return false
85         }
86         for (i in attr1.indices) {
87             if (attr1[i].name != attr2[i].name) {
88                 return false
89             }
90             val value1 = attr1[i].value ?: return false
91             val value2 = attr2[i].value ?: return false
92             // Try to compare values directly with each other.
93             val v1 = ConstantEvaluator.evaluate(context, value1)
94             val v2 = ConstantEvaluator.evaluate(context, value2)
95             if (v1 != null && v2 != null) {
96                 if (v1 != v2) {
97                     return false
98                 }
99             } else {
100                 val children1 = annotationValueGetChildren(value1)
101                 val children2 = annotationValueGetChildren(value2)
102                 if (children1.size != children2.size) {
103                     return false
104                 }
105                 for (j in children1.indices) {
106                     val c1 = ConstantEvaluator.evaluate(context, children1[j])
107                     val c2 = ConstantEvaluator.evaluate(context, children2[j])
108                     if (c1 != c2) {
109                         return false
110                     }
111                 }
112             }
113         }
114         return true
115     }
116 
117     private fun compareMethods(
118         context: JavaContext,
119         element: UElement,
120         overridingMethod: PsiMethod,
121         overriddenMethod: PsiMethod,
122         checkEquivalence: Boolean = true
123     ) {
124         // If method is not from a Stub subclass, this method shouldn't use @EP at all.
125         // This is handled by EnforcePermissionHelperDetector.
126         if (!isContainedInSubclassOfStub(context, overridingMethod.toUElement() as? UMethod)) {
127             return
128         }
129         val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
130         val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
131         val location = context.getLocation(element)
132         val overridingClass = overridingMethod.parent as PsiClass
133         val overriddenClass = overriddenMethod.parent as PsiClass
134         val overridingName = "${overridingClass.name}.${overridingMethod.name}"
135         val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}"
136         if (overridingAnnotation == null) {
137             val msg = "The method $overridingName overrides the method $overriddenName which " +
138                 "is annotated with @EnforcePermission. The same annotation must be used " +
139                 "on $overridingName"
140             context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
141         } else if (overriddenAnnotation == null) {
142             val msg = "The method $overridingName overrides the method $overriddenName which " +
143                 "is not annotated with @EnforcePermission. The same annotation must be " +
144                 "used on $overriddenName. Did you forget to annotate the AIDL definition?"
145             context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
146         } else if (checkEquivalence && !areAnnotationsEquivalent(
147                     context, overridingAnnotation, overriddenAnnotation)) {
148             val msg = "The method $overridingName is annotated with " +
149                 "${overridingAnnotation.text} which differs from the overridden " +
150                 "method $overriddenName: ${overriddenAnnotation.text}. The same " +
151                 "annotation must be used for both methods."
152             context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
153         }
154     }
155 
156     override fun visitAnnotationUsage(
157         context: JavaContext,
158         element: UElement,
159         annotationInfo: AnnotationInfo,
160         usageInfo: AnnotationUsageInfo
161     ) {
162         if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
163             annotationInfo.origin == AnnotationOrigin.METHOD) {
164             val overridingMethod = element.sourcePsi as PsiMethod
165             val overriddenMethod = usageInfo.referenced as PsiMethod
166             compareMethods(context, element, overridingMethod, overriddenMethod)
167         }
168     }
169 
170     override fun createUastHandler(context: JavaContext): UElementHandler {
171         return object : UElementHandler() {
172             override fun visitAnnotation(node: UAnnotation) {
173                 if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) {
174                     return
175                 }
176                 val method = node.uastParent as? UMethod ?: return
177                 val overridingMethod = method as PsiMethod
178                 val parents = overridingMethod.findSuperMethods()
179                 for (overriddenMethod in parents) {
180                     // The equivalence check can be skipped, if both methods are
181                     // annotated, it will be verified by visitAnnotationUsage.
182                     compareMethods(context, method, overridingMethod,
183                         overriddenMethod, checkEquivalence = false)
184                 }
185             }
186         }
187     }
188 
189     companion object {
190         val EXPLANATION = """
191             The @EnforcePermission annotation is used to indicate that the underlying binder code
192             has already verified the caller's permissions before calling the appropriate method. The
193             verification code is usually generated by the AIDL compiler, which also takes care of
194             annotating the generated Java code.
195 
196             In order to surface that information to platform developers, the same annotation must be
197             used on the implementation class or methods.
198             """
199 
200         val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create(
201             id = "MissingEnforcePermissionAnnotation",
202             briefDescription = "Missing @EnforcePermission annotation on Binder method",
203             explanation = EXPLANATION,
204             category = Category.SECURITY,
205             priority = 6,
206             severity = Severity.ERROR,
207             implementation = Implementation(
208                     EnforcePermissionDetector::class.java,
209                     Scope.JAVA_FILE_SCOPE
210             )
211         )
212 
213         val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create(
214             id = "MismatchingEnforcePermissionAnnotation",
215             briefDescription = "Incorrect @EnforcePermission annotation on Binder method",
216             explanation = EXPLANATION,
217             category = Category.SECURITY,
218             priority = 6,
219             severity = Severity.ERROR,
220             implementation = Implementation(
221                     EnforcePermissionDetector::class.java,
222                     Scope.JAVA_FILE_SCOPE
223             )
224         )
225     }
226 }
227