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.packageinstaller.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.packageinstaller.Constants; 33 import com.android.packageinstaller.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