blob: 29771418b530505c8ffb9248c7ddeb98c02784df [file] [log] [blame]
/*
* Copyright (C) 2016 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.
*/
package com.android.tools.lint.checks;
import static com.android.tools.lint.detector.api.Lint.getMethodName;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiVariable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UCatchClause;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/** Detect calls to get device Identifiers. */
public class HardwareIdDetector extends Detector implements SourceCodeScanner {
private static final Implementation IMPLEMENTATION =
new Implementation(HardwareIdDetector.class, Scope.JAVA_FILE_SCOPE);
/** Hardware Id Usages */
public static final Issue ISSUE =
Issue.create(
"HardwareIds",
"Hardware Id Usage",
"Using these device identifiers is not recommended "
+ "other than for high value fraud prevention and advanced telephony use-cases. "
+ "For advertising use-cases, use `AdvertisingIdClient$Info#getId` and for "
+ "analytics, use `InstanceId#getId`.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"https://developer.android.com/training/articles/user-data-ids.html")
.setAndroidSpecific(true);
private static final String BLUETOOTH_ADAPTER_GET_ADDRESS = "getAddress";
private static final String WIFI_INFO_GET_MAC_ADDRESS = "getMacAddress";
private static final String TELEPHONY_MANAGER_GET_DEVICE_ID = "getDeviceId";
private static final String TELEPHONY_MANAGER_GET_LINE_1_NUMBER = "getLine1Number";
private static final String TELEPHONY_MANAGER_GET_SIM_SERIAL_NUMBER = "getSimSerialNumber";
private static final String TELEPHONY_MANAGER_GET_SUBSCRIBER_ID = "getSubscriberId";
private static final String SETTINGS_SECURE_GET_STRING = "getString";
private static final String PLAY_SERVICES_NOT_AVAILABLE_EXCEPTION =
"com.google.android.gms.common.GooglePlayServicesNotAvailableException";
private static final String MESSAGE_DEVICE_IDENTIFIERS =
"Using `%1$s` to get device identifiers is not recommended";
private static final String RO_SERIALNO = "ro.serialno";
private static final String CLASS_FOR_NAME = "forName";
private static final String CLASSLOADER_LOAD_CLASS = "loadClass";
/** Constructs a new {@link HardwareIdDetector} */
public HardwareIdDetector() {}
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
BLUETOOTH_ADAPTER_GET_ADDRESS,
WIFI_INFO_GET_MAC_ADDRESS,
TELEPHONY_MANAGER_GET_DEVICE_ID,
TELEPHONY_MANAGER_GET_LINE_1_NUMBER,
TELEPHONY_MANAGER_GET_SIM_SERIAL_NUMBER,
TELEPHONY_MANAGER_GET_SUBSCRIBER_ID,
SETTINGS_SECURE_GET_STRING,
CLASS_FOR_NAME,
CLASSLOADER_LOAD_CLASS);
}
@Override
public void visitMethodCall(
@NonNull JavaContext context,
@NonNull UCallExpression node,
@NonNull PsiMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
String className;
String methodName = method.getName();
switch (methodName) {
case BLUETOOTH_ADAPTER_GET_ADDRESS:
className = "android.bluetooth.BluetoothAdapter";
break;
case WIFI_INFO_GET_MAC_ADDRESS:
className = "android.net.wifi.WifiInfo";
break;
case TELEPHONY_MANAGER_GET_DEVICE_ID:
case TELEPHONY_MANAGER_GET_LINE_1_NUMBER:
case TELEPHONY_MANAGER_GET_SIM_SERIAL_NUMBER:
case TELEPHONY_MANAGER_GET_SUBSCRIBER_ID:
className = "android.telephony.TelephonyManager";
break;
case SETTINGS_SECURE_GET_STRING:
className = "android.provider.Settings.Secure";
break;
case CLASS_FOR_NAME:
className = "java.lang.Class";
break;
case CLASSLOADER_LOAD_CLASS:
className = "java.lang.ClassLoader";
break;
default:
// call.getMethodName() and resolvedMethod.getName() may not always
// be identical, such as in the case of module private methods in
// Kotlin, but that's not an issue for the SDK methods we're looking
// up here
return;
}
if (!evaluator.isMemberInClass(method, className)) {
return;
}
if (methodName.equals(SETTINGS_SECURE_GET_STRING)) {
if (evaluator.getParameterCount(method) != 2 || node.getValueArgumentCount() != 2) {
// we are explicitly looking for Secure.getString(x, ANDROID_ID) here
return;
}
String value =
ConstantEvaluator.evaluateString(
context, node.getValueArguments().get(1), false);
// Check if the value matches Settings.Secure.ANDROID_ID
if (!"android_id".equals(value)) {
return;
}
// The 2nd parameter resolved to the constant value Settings.Secure.ANDROID_ID
// which is not recommended so continue and show an error.
} else if (methodName.equals(CLASS_FOR_NAME) || methodName.equals(CLASSLOADER_LOAD_CLASS)) {
// Here we are looking for usages of
// `android.os.SystemProperties.get("ro.serialno")` using reflection.
//
// Typical code for this would looks like the following:
//
// Class<?> c = Class.forName("android.os.SystemProperties");
// Method get = c.getMethod("get", String.class);
// ...result = (String) get.invoke(null, "ro.serialno");
//
findReflectionUsage(node, context);
return;
}
// If any of the calls to get device identifiers are explicitly in a catch block for
// GooglePlayServicesNotAvailableException, then don't report a warning.
// This is to handle the case where the alternate play services api is unavailable on
// the device.
if (inCatchPlayServicesNotAvailableException(node)) {
return;
}
String message = String.format(MESSAGE_DEVICE_IDENTIFIERS, methodName);
context.report(ISSUE, node, context.getLocation(node), message);
}
@Nullable
@Override
public List<String> getApplicableReferenceNames() {
return Collections.singletonList("SERIAL");
}
@Override
public void visitReference(
@NonNull JavaContext context,
@NonNull UReferenceExpression reference,
@NonNull PsiElement resolved) {
JavaEvaluator evaluator = context.getEvaluator();
if (resolved instanceof PsiField
&& evaluator.isMemberInSubClassOf((PsiField) resolved, "android.os.Build", false)) {
String message = String.format(MESSAGE_DEVICE_IDENTIFIERS, "SERIAL");
context.report(ISSUE, reference, context.getNameLocation(reference), message);
}
}
/**
* Check if the given expression is within a catch block of {@code
* PLAY_SERVICES_NOT_AVAILABLE_EXCEPTION}
*
* @param expression PsiExpression that can be within a catch block
* @return true iff the expression is within the catch block
*/
private static boolean inCatchPlayServicesNotAvailableException(UExpression expression) {
UCatchClause surroundingCatchSection =
UastUtils.getParentOfType(expression, UCatchClause.class, true);
if (surroundingCatchSection != null) {
for (PsiType t : surroundingCatchSection.getTypes()) {
if (t.equalsToText(PLAY_SERVICES_NOT_AVAILABLE_EXCEPTION)) {
return true;
}
}
}
return false;
}
private static void findReflectionUsage(
@NonNull UCallExpression expression, @NonNull JavaContext context) {
List<UExpression> methodArgs = expression.getValueArguments();
if (methodArgs.isEmpty()) {
return;
}
String value = ConstantEvaluator.evaluateString(context, methodArgs.get(0), false);
if (!"android.os.SystemProperties".equals(value)) {
return;
}
UMethod surroundingMethod = UastUtils.getParentOfType(expression, UMethod.class, true);
if (surroundingMethod == null) {
return;
}
InvokeCallVisitor visitor = new InvokeCallVisitor(context, expression);
surroundingMethod.accept(visitor);
PsiParameter argExpression = visitor.getPsiParameter();
if (argExpression == null) {
// If RO_SERIALNO string was found, the warning is already reported by the
// visitor.
return;
}
// The key was passed into the method as a parameter into the given method.
// e.g. getSystemProperty(context, "ro.serialno")
// So we need to find calls to the current method. Note: Here we restrict
// the search to the current class.
UClass surroundingClass = UastUtils.getContainingUClass(surroundingMethod);
if (surroundingClass != null) {
int paramIndex = surroundingMethod.getParameterList().getParameterIndex(argExpression);
if (paramIndex < 0) {
return;
}
FindMethodCallVisitor methodCallVisitor =
new FindMethodCallVisitor(context, surroundingMethod.getPsi(), paramIndex);
surroundingClass.accept(methodCallVisitor);
}
}
/**
* Search for a sequence of reflection methods calls leading to {@link
* java.lang.reflect.Method#invoke(Object, Object...)} and also check the parameter(s) passed
* into invoke.
*/
private static final class InvokeCallVisitor extends AbstractUastVisitor {
private final UCallExpression mLoadMethod;
private final JavaContext mContext;
private String mLoadVariable;
private String mMethodVariable;
private boolean mProcessingDone;
private PsiParameter mPsiParameter;
public InvokeCallVisitor(JavaContext context, UCallExpression expression) {
mContext = context;
mLoadMethod = expression;
}
@Override
public boolean visitElement(UElement node) {
return mProcessingDone || super.visitElement(node);
}
@Override
public boolean visitCallExpression(UCallExpression expression) {
if (expression.equals(mLoadMethod)) {
PsiVariable variable = CleanupDetector.getVariableElement(expression, true, false);
// PsiVariable variable = UastUtils.getParentOfType(expression, UVariable.class);
mLoadVariable = variable == null ? null : variable.getName();
} else if (mLoadVariable != null
&& isDesiredMethodCall(
expression, mLoadVariable, "java.lang.Class", "getMethod", 0)) {
// clazz.getMethod("get", ..)
UExpression arg = methodParameterAt(expression, 0 /* param index */);
String value = ConstantEvaluator.evaluateString(mContext, arg, false);
if ("get".equals(value)) {
PsiVariable variable = CleanupDetector.getVariableElement(expression);
mMethodVariable = variable == null ? null : variable.getName();
}
} else if (mMethodVariable != null
&& isDesiredMethodCall(
expression,
mMethodVariable,
"java.lang.reflect.Method",
"invoke",
1 /* param index */)) {
// method.invoke(instance, "ro.serialno")
UExpression arg = methodParameterAt(expression, 1);
String value = ConstantEvaluator.evaluateString(mContext, arg, false);
if (RO_SERIALNO.equals(value)) {
mContext.report(
ISSUE,
arg,
mContext.getLocation(arg),
String.format(MESSAGE_DEVICE_IDENTIFIERS, RO_SERIALNO));
} else if (arg instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) arg).resolve();
if (resolved instanceof PsiParameter) {
mPsiParameter = (PsiParameter) resolved;
}
}
mProcessingDone = true;
}
return super.visitCallExpression(expression);
}
@Nullable
PsiParameter getPsiParameter() {
return mPsiParameter;
}
private static UExpression methodParameterAt(UCallExpression expression, int index) {
List<UExpression> expressions = expression.getValueArguments();
assert expressions.size() > index;
return expressions.get(index);
}
private static boolean isDesiredMethodCall(
@NonNull UCallExpression expression,
@NonNull String variableQualifier,
@NonNull String containingClass,
@NonNull String desiredMethodName,
int paramIndex) {
if (!desiredMethodName.equals(getMethodName(expression))) {
return false;
}
// Check that the qualifier used is the same.
UExpression qualifierExpression = expression.getReceiver();
if (qualifierExpression == null
|| !variableQualifier.equals(qualifierExpression.asSourceString())) {
return false;
}
PsiMethod method = expression.resolve();
return method != null
&& method.getContainingClass() != null
&& containingClass.equals(method.getContainingClass().getQualifiedName())
&& expression.getValueArgumentCount() > paramIndex;
}
}
/**
* Find calls to a given method and report an issue if a parameter at parametIndex evaluates to
* a constant 'ro.serialno'
*/
private static final class FindMethodCallVisitor extends AbstractUastVisitor {
private final JavaContext mContext;
private final PsiMethod mPsiMethod;
private final int mParamIndex;
FindMethodCallVisitor(JavaContext context, PsiMethod method, int paramIndex) {
mContext = context;
mPsiMethod = method;
mParamIndex = paramIndex;
}
@Override
public boolean visitCallExpression(UCallExpression expression) {
if (mPsiMethod.equals(expression.resolve())) {
List<UExpression> expressions = expression.getValueArguments();
if (expressions.size() > mParamIndex) {
UExpression paramExpr = expressions.get(mParamIndex);
String value = ConstantEvaluator.evaluateString(mContext, paramExpr, false);
if (RO_SERIALNO.equals(value)
&& !inCatchPlayServicesNotAvailableException(expression)) {
String message = String.format(MESSAGE_DEVICE_IDENTIFIERS, RO_SERIALNO);
mContext.report(ISSUE, paramExpr, mContext.getLocation(paramExpr), message);
}
}
}
return super.visitCallExpression(expression);
}
}
}