All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.HardwareIdDetector Maven / Gradle / Ivy

The newest version!
/*
 * 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 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.intellij.psi.JavaElementVisitor;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiCatchSection;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDisjunctionType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.PsiTreeUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Detect calls to get device Identifiers.
 */
public class HardwareIdDetector extends Detector implements Detector.JavaPsiScanner {

    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");

    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 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 visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
            @NonNull PsiMethodCallExpression node, @NonNull PsiMethod method) {
        JavaEvaluator evaluator = context.getEvaluator();
        String className = null;
        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:
                assert false;
        }

        if (!evaluator.isMemberInClass(method, className)) {
            return;
        }

        if (methodName.equals(SETTINGS_SECURE_GET_STRING)) {
            if (evaluator.getParameterCount(method) != 2
                    || node.getArgumentList().getExpressions().length != 2) {
                // we are explicitly looking for Secure.getString(x, ANDROID_ID) here
                return;
            }
            String value = ConstantEvaluator.evaluateString(
                    context, node.getArgumentList().getExpressions()[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 getApplicableReferenceNames() {
        return Collections.singletonList("SERIAL");
    }

    @Override
    public void visitReference(
            @NonNull JavaContext context,
            @Nullable JavaElementVisitor visitor,
            @NonNull PsiJavaCodeReferenceElement 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(PsiExpression expression) {

        PsiCatchSection surroundingCatchSection =
                PsiTreeUtil.getParentOfType(expression, PsiCatchSection.class, true);

        if (surroundingCatchSection != null && surroundingCatchSection.getCatchType() != null) {
            PsiType catchType = surroundingCatchSection.getCatchType();
            // Handle multi-catch statements such as (IOException | AnotherException e)
            if (catchType instanceof PsiDisjunctionType) {
                PsiDisjunctionType disjunctionType = (PsiDisjunctionType) catchType;
                if (disjunctionType.getDisjunctions()
                        .stream()
                        .anyMatch(t -> t.equalsToText(PLAY_SERVICES_NOT_AVAILABLE_EXCEPTION))) {
                    return true;
                }
            } else if (catchType.equalsToText(PLAY_SERVICES_NOT_AVAILABLE_EXCEPTION)) {
                return true;
            }
        }
        return false;
    }

    private static void findReflectionUsage(@NonNull PsiMethodCallExpression expression,
            @NonNull JavaContext context) {
        PsiExpression[] methodArgs = expression.getArgumentList().getExpressions();
        if (methodArgs.length < 1) {
            return;
        }
        String value = ConstantEvaluator.evaluateString(context, methodArgs[0], false);
        if (!"android.os.SystemProperties".equals(value)) {
            return;
        }
        PsiMethod surroundingMethod =
                PsiTreeUtil.getParentOfType(expression, PsiMethod.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.
        PsiClass surroundingClass = PsiTreeUtil.getParentOfType(
                surroundingMethod, PsiClass.class, true);

        if (surroundingClass != null) {
            int paramIndex = surroundingMethod.getParameterList()
                    .getParameterIndex(argExpression);
            if (paramIndex < 0) {
                return;
            }
            FindMethodCallVisitor methodCallVisitor =
                    new FindMethodCallVisitor(context, surroundingMethod, 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 JavaRecursiveElementVisitor {

        private final PsiMethodCallExpression mLoadMethod;
        private final JavaContext mContext;

        private String mLoadVariable;
        private String mMethodVariable;
        private boolean mProcessingDone;
        private PsiParameter mPsiParameter;

        public InvokeCallVisitor(JavaContext context, PsiMethodCallExpression expression) {
            mContext = context;
            mLoadMethod = expression;
        }

        @Override
        public void visitElement(PsiElement element) {
            // stop processing if we have already concluded our search.
            if (!mProcessingDone) {
                super.visitElement(element);
            }
        }

        @Override
        public void visitMethodCallExpression(PsiMethodCallExpression expression) {
            super.visitMethodCallExpression(expression);
            if (expression == mLoadMethod) {
                PsiVariable variable = CleanupDetector.getVariableElement(expression);
                mLoadVariable = variable == null ? null : variable.getName();
            } else if (mLoadVariable != null
                    && isDesiredMethodCall(expression, mLoadVariable,
                    "java.lang.Class", "getMethod", 0)) {
                // clazz.getMethod("get", ..)
                PsiExpression 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")
                PsiExpression 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 PsiReferenceExpression) {
                    PsiElement resolved = ((PsiReferenceExpression)arg).resolve();
                    if (resolved instanceof PsiParameter) {
                        mPsiParameter = (PsiParameter)resolved;
                    }
                }
                mProcessingDone = true;
            }
        }

        @Nullable
        PsiParameter getPsiParameter() {
            return mPsiParameter;
        }

        private static PsiExpression methodParameterAt(PsiMethodCallExpression expression,
                int index) {
            PsiExpression[] expressions = expression.getArgumentList().getExpressions();
            assert expressions.length > index;
            return expressions[index];
        }

        private static boolean isDesiredMethodCall(@NonNull PsiMethodCallExpression expression,
                @NonNull String variableQualifier,
                @NonNull String containingClass,
                @NonNull String desiredMethodName, int paramIndex) {

            if (!desiredMethodName.equals(expression.getMethodExpression().getReferenceName())) {
                return false;
            }
            // Check that the qualifier used is the same.
            PsiExpression qualifierExpression = expression
                    .getMethodExpression()
                    .getQualifierExpression();

            if (qualifierExpression == null
                    || !variableQualifier.equals(qualifierExpression.getText())) {
                return false;
            }

            PsiMethod method = expression.resolveMethod();

            return method != null
                    && method.getContainingClass() != null
                    && containingClass.equals(method.getContainingClass().getQualifiedName())
                    && expression.getArgumentList().getExpressions().length > 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 JavaRecursiveElementVisitor {

        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 void visitMethodCallExpression(PsiMethodCallExpression expression) {
            super.visitMethodCallExpression(expression);
            if (mPsiMethod == expression.resolveMethod()) {
                PsiExpression[] expressions = expression.getArgumentList().getExpressions();
                if (expressions.length > mParamIndex) {
                    PsiExpression paramExpr = expressions[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);
                    }
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy