1 /*
2  * Copyright (C) 2019 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.android.permissioncontroller.permission.service;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.SharedPreferences;
22 import android.database.Cursor;
23 import android.database.MatrixCursor;
24 import android.provider.SearchIndexablesContract;
25 import android.provider.SearchIndexablesProvider;
26 import android.util.Log;
27 
28 import androidx.annotation.CheckResult;
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 
32 import com.android.permissioncontroller.Constants;
33 import com.android.permissioncontroller.permission.utils.Utils;
34 
35 import java.util.Objects;
36 import java.util.UUID;
37 
38 /**
39  * Base class for {@link SearchIndexablesProvider} inside permission controller, which allows using
40  * a password in raw data key and verifying incoming intents afterwards.
41  */
42 public abstract class BaseSearchIndexablesProvider extends SearchIndexablesProvider {
43 
44     private static final String LOG_TAG = BaseSearchIndexablesProvider.class.getSimpleName();
45 
46     private static final String EXTRA_SETTINGS_SEARCH_KEY = ":settings:fragment_args_key";
47 
48     private static final int PASSWORD_LENGTH = 36;
49 
50     @NonNull
51     private static final Object sPasswordLock = new Object();
52 
53     @Override
onCreate()54     public boolean onCreate() {
55         return true;
56     }
57 
58     @Nullable
59     @Override
queryXmlResources(@ullable String[] projection)60     public Cursor queryXmlResources(@Nullable String[] projection) {
61         return new MatrixCursor(SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS);
62     }
63 
64     @Nullable
65     @Override
queryNonIndexableKeys(@ullable String[] projection)66     public Cursor queryNonIndexableKeys(@Nullable String[] projection) {
67         return new MatrixCursor(SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
68     }
69 
70     @NonNull
getPassword(@onNull Context context)71     private static String getPassword(@NonNull Context context) {
72         synchronized (sPasswordLock) {
73             SharedPreferences sharedPreferences = Utils.getDeviceProtectedSharedPreferences(
74                     context);
75             String password = sharedPreferences.getString(
76                     Constants.SEARCH_INDEXABLE_PROVIDER_PASSWORD_KEY, null);
77             if (password == null) {
78                 password = UUID.randomUUID().toString();
79                 sharedPreferences.edit()
80                         .putString(Constants.SEARCH_INDEXABLE_PROVIDER_PASSWORD_KEY, password)
81                         .apply();
82             }
83             return password;
84         }
85     }
86 
87     /**
88      * Create a unique raw data key with password.
89      *
90      * @param key the original key, can be retrieved later with {@link #getOriginalKey(Intent)}
91      * @param context the context to use
92      * @return the created raw data key
93      */
94     @NonNull
createRawDataKey(@onNull String key, @NonNull Context context)95     protected static String createRawDataKey(@NonNull String key, @NonNull Context context) {
96         return getPassword(context) + context.getPackageName() + ',' + key;
97     }
98 
99     /**
100      * Check if the intent contains the properties expected from an intent launched from settings
101      * search.
102      *
103      * @param intent the intent to check
104      * @param context the context to get password
105      *
106      * @return whether the intent is valid
107      */
108     @CheckResult
isIntentValid(@onNull Intent intent, @NonNull Context context)109     public static boolean isIntentValid(@NonNull Intent intent, @NonNull Context context) {
110         String key = intent.getStringExtra(EXTRA_SETTINGS_SEARCH_KEY);
111         String passwordFromIntent = key.substring(0, PASSWORD_LENGTH);
112         String password = getPassword(context);
113         boolean verified = Objects.equals(passwordFromIntent, password);
114         if (!verified) {
115             Log.w(LOG_TAG, "Invalid password: " + passwordFromIntent);
116         }
117         return verified;
118     }
119 
120     /**
121      * Get the original key passed to {@link #createRawDataKey(String, Context)}. Should only be
122      * called after {@link #isIntentValid(Intent, Context)}.
123      *
124      * @param intent the intent to get the original key
125      *
126      * @return the original key from the intent, or {@code null} if none
127      */
128     @Nullable
getOriginalKey(@onNull Intent intent)129     public static String getOriginalKey(@NonNull Intent intent) {
130         String key = intent.getStringExtra(EXTRA_SETTINGS_SEARCH_KEY);
131         if (key == null) {
132             return null;
133         }
134         int keyStart = key.indexOf(',') + 1;
135         return keyStart <= key.length() ? key.substring(keyStart) : null;
136     }
137 }
138