1 /* 2 * Copyright (C) 2020 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.settings.security; 18 19 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 20 21 import android.annotation.Nullable; 22 import android.app.Activity; 23 import android.app.admin.DevicePolicyEventLogger; 24 import android.app.admin.DevicePolicyManager; 25 import android.content.Context; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.content.pm.UserInfo; 29 import android.content.res.Configuration; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.HandlerThread; 34 import android.os.Process; 35 import android.os.RemoteException; 36 import android.os.UserManager; 37 import android.security.AppUriAuthenticationPolicy; 38 import android.security.Credentials; 39 import android.security.KeyChain; 40 import android.stats.devicepolicy.DevicePolicyEnums; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.View; 44 import android.widget.Button; 45 import android.widget.ImageView; 46 import android.widget.LinearLayout; 47 import android.widget.TextView; 48 49 import androidx.annotation.NonNull; 50 import androidx.annotation.VisibleForTesting; 51 import androidx.recyclerview.widget.LinearLayoutManager; 52 import androidx.recyclerview.widget.RecyclerView; 53 54 import com.android.settings.R; 55 56 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; 57 58 import java.util.Map; 59 60 /** 61 * Displays a full screen to the user asking whether the calling app can manage the user's 62 * KeyChain credentials. This screen includes the authentication policy highlighting what apps and 63 * URLs the calling app can authenticate the user to. 64 * <p> 65 * Users can allow or deny the calling app. If denied, the calling app may re-request this 66 * capability. If allowed, the calling app will become the credential management app and will be 67 * able to manage the user's KeyChain credentials. The following APIs can be called to manage 68 * KeyChain credentials: 69 * {@link DevicePolicyManager#installKeyPair} 70 * {@link DevicePolicyManager#removeKeyPair} 71 * {@link DevicePolicyManager#generateKeyPair} 72 * {@link DevicePolicyManager#setKeyPairCertificate} 73 * <p> 74 * 75 * @see AppUriAuthenticationPolicy 76 */ 77 public class RequestManageCredentials extends Activity { 78 79 private static final String TAG = "ManageCredentials"; 80 81 private String mCredentialManagerPackage; 82 private AppUriAuthenticationPolicy mAuthenticationPolicy; 83 84 private RecyclerView mRecyclerView; 85 private LinearLayoutManager mLayoutManager; 86 private LinearLayout mButtonPanel; 87 private ExtendedFloatingActionButton mExtendedFab; 88 89 private HandlerThread mKeyChainTread; 90 private KeyChain.KeyChainConnection mKeyChainConnection; 91 92 private boolean mDisplayingButtonPanel = false; 93 private boolean mIsLandscapeMode = false; 94 95 @Override onCreate(@ullable Bundle savedInstanceState)96 public void onCreate(@Nullable Bundle savedInstanceState) { 97 super.onCreate(savedInstanceState); 98 99 if (!Credentials.ACTION_MANAGE_CREDENTIALS.equals(getIntent().getAction())) { 100 Log.e(TAG, "Unable to start activity because intent action is not " 101 + Credentials.ACTION_MANAGE_CREDENTIALS); 102 logRequestFailure(); 103 finishWithResultCancelled(); 104 return; 105 } 106 if (isManagedDevice()) { 107 Log.e(TAG, "Credential management on managed devices should be done by the Device " 108 + "Policy Controller, not a credential management app"); 109 logRequestFailure(); 110 finishWithResultCancelled(); 111 return; 112 } 113 mCredentialManagerPackage = getLaunchedFromPackage(); 114 if (TextUtils.isEmpty(mCredentialManagerPackage)) { 115 Log.e(TAG, "Unknown credential manager app"); 116 logRequestFailure(); 117 finishWithResultCancelled(); 118 return; 119 } 120 DevicePolicyEventLogger 121 .createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_NAME) 122 .setStrings(mCredentialManagerPackage) 123 .write(); 124 setContentView(R.layout.request_manage_credentials); 125 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 126 mIsLandscapeMode = getResources().getConfiguration().orientation 127 == Configuration.ORIENTATION_LANDSCAPE; 128 129 mKeyChainTread = new HandlerThread("KeyChainConnection"); 130 mKeyChainTread.start(); 131 mKeyChainConnection = getKeyChainConnection(this, mKeyChainTread); 132 133 AppUriAuthenticationPolicy policy = 134 getIntent().getParcelableExtra(KeyChain.EXTRA_AUTHENTICATION_POLICY); 135 if (!isValidAuthenticationPolicy(policy)) { 136 Log.e(TAG, "Invalid authentication policy"); 137 logRequestFailure(); 138 finishWithResultCancelled(); 139 return; 140 } 141 mAuthenticationPolicy = policy; 142 DevicePolicyEventLogger 143 .createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_POLICY) 144 .setStrings(getNumberOfAuthenticationPolicyApps(mAuthenticationPolicy), 145 getNumberOfAuthenticationPolicyUris(mAuthenticationPolicy)) 146 .write(); 147 148 if (mIsLandscapeMode) { 149 loadHeader(); 150 } 151 loadRecyclerView(); 152 loadButtons(); 153 loadExtendedFloatingActionButton(); 154 addOnScrollListener(); 155 } 156 157 @Override onDestroy()158 protected void onDestroy() { 159 super.onDestroy(); 160 if (mKeyChainConnection != null) { 161 mKeyChainConnection.close(); 162 mKeyChainConnection = null; 163 mKeyChainTread.quitSafely(); 164 } 165 } 166 isValidAuthenticationPolicy(AppUriAuthenticationPolicy policy)167 private boolean isValidAuthenticationPolicy(AppUriAuthenticationPolicy policy) { 168 if (policy == null || policy.getAppAndUriMappings().isEmpty()) { 169 return false; 170 } 171 try { 172 // Check whether any of the aliases in the policy already exist 173 for (String alias : policy.getAliases()) { 174 if (mKeyChainConnection.getService().requestPrivateKey(alias) != null) { 175 return false; 176 } 177 } 178 } catch (RemoteException e) { 179 Log.e(TAG, "Invalid authentication policy", e); 180 return false; 181 } 182 return true; 183 } 184 isManagedDevice()185 private boolean isManagedDevice() { 186 DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class); 187 188 return dpm.getDeviceOwnerUser() != null 189 || dpm.getProfileOwner() != null 190 || hasManagedProfile(); 191 } 192 hasManagedProfile()193 private boolean hasManagedProfile() { 194 UserManager um = getSystemService(UserManager.class); 195 for (final UserInfo userInfo : um.getProfiles(getUserId())) { 196 if (userInfo.isManagedProfile()) { 197 return true; 198 } 199 } 200 return false; 201 } 202 loadRecyclerView()203 private void loadRecyclerView() { 204 mLayoutManager = new LinearLayoutManager(this); 205 mRecyclerView = findViewById(R.id.apps_list); 206 mRecyclerView.setLayoutManager(mLayoutManager); 207 208 CredentialManagementAppAdapter recyclerViewAdapter = new CredentialManagementAppAdapter( 209 this, mCredentialManagerPackage, mAuthenticationPolicy.getAppAndUriMappings(), 210 /* include header= */ !mIsLandscapeMode, /* include expander= */ false); 211 mRecyclerView.setAdapter(recyclerViewAdapter); 212 } 213 loadButtons()214 private void loadButtons() { 215 mButtonPanel = findViewById(R.id.button_panel); 216 Button dontAllowButton = findViewById(R.id.dont_allow_button); 217 dontAllowButton.setFilterTouchesWhenObscured(true); 218 Button allowButton = findViewById(R.id.allow_button); 219 allowButton.setFilterTouchesWhenObscured(true); 220 221 dontAllowButton.setOnClickListener(b -> { 222 DevicePolicyEventLogger 223 .createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_DENIED) 224 .write(); 225 finishWithResultCancelled(); 226 }); 227 allowButton.setOnClickListener(b -> setOrUpdateCredentialManagementAppAndFinish()); 228 } 229 loadExtendedFloatingActionButton()230 private void loadExtendedFloatingActionButton() { 231 mExtendedFab = findViewById(R.id.extended_fab); 232 mExtendedFab.setOnClickListener(v -> { 233 final int position = mIsLandscapeMode 234 ? mAuthenticationPolicy.getAppAndUriMappings().size() - 1 235 : mAuthenticationPolicy.getAppAndUriMappings().size(); 236 mRecyclerView.scrollToPosition(position); 237 mExtendedFab.hide(); 238 showButtonPanel(); 239 }); 240 } 241 loadHeader()242 private void loadHeader() { 243 final ImageView mAppIconView = findViewById(R.id.credential_management_app_icon); 244 final TextView mTitleView = findViewById(R.id.credential_management_app_title); 245 try { 246 ApplicationInfo applicationInfo = 247 getPackageManager().getApplicationInfo(mCredentialManagerPackage, 0); 248 mAppIconView.setImageDrawable(getPackageManager().getApplicationIcon(applicationInfo)); 249 mTitleView.setText(TextUtils.expandTemplate( 250 getText(R.string.request_manage_credentials_title), 251 applicationInfo.loadLabel(getPackageManager()))); 252 } catch (PackageManager.NameNotFoundException e) { 253 mAppIconView.setImageDrawable(null); 254 mTitleView.setText(TextUtils.expandTemplate( 255 getText(R.string.request_manage_credentials_title), 256 mCredentialManagerPackage)); 257 } 258 } 259 setOrUpdateCredentialManagementAppAndFinish()260 private void setOrUpdateCredentialManagementAppAndFinish() { 261 try { 262 mKeyChainConnection.getService().setCredentialManagementApp( 263 mCredentialManagerPackage, mAuthenticationPolicy); 264 DevicePolicyEventLogger 265 .createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_ACCEPTED) 266 .write(); 267 setResult(RESULT_OK); 268 } catch (RemoteException e) { 269 Log.e(TAG, "Unable to set credential manager app", e); 270 logRequestFailure(); 271 } 272 finish(); 273 } 274 275 @VisibleForTesting getKeyChainConnection(Context context, HandlerThread thread)276 KeyChain.KeyChainConnection getKeyChainConnection(Context context, HandlerThread thread) { 277 final Handler handler = new Handler(thread.getLooper()); 278 try { 279 KeyChain.KeyChainConnection connection = KeyChain.bindAsUser( 280 context, handler, Process.myUserHandle()); 281 return connection; 282 } catch (InterruptedException e) { 283 throw new RuntimeException("Faile to bind to KeyChain", e); 284 } 285 } 286 addOnScrollListener()287 private void addOnScrollListener() { 288 mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 289 @Override 290 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 291 super.onScrolled(recyclerView, dx, dy); 292 if (!mDisplayingButtonPanel) { 293 // On down scroll, hide text in floating action button by setting 294 // extended to false. 295 if (dy > 0 && mExtendedFab.getVisibility() == View.VISIBLE) { 296 mExtendedFab.shrink(); 297 } 298 if (isRecyclerScrollable()) { 299 mExtendedFab.show(); 300 hideButtonPanel(); 301 } else { 302 mExtendedFab.hide(); 303 showButtonPanel(); 304 } 305 } 306 } 307 }); 308 } 309 showButtonPanel()310 private void showButtonPanel() { 311 // Add padding to remove overlap between recycler view and button panel. 312 int padding_in_px = (int) (60 * getResources().getDisplayMetrics().density + 0.5f); 313 mRecyclerView.setPadding(0, 0, 0, padding_in_px); 314 mButtonPanel.setVisibility(View.VISIBLE); 315 mDisplayingButtonPanel = true; 316 } 317 hideButtonPanel()318 private void hideButtonPanel() { 319 mRecyclerView.setPadding(0, 0, 0, 0); 320 mButtonPanel.setVisibility(View.GONE); 321 } 322 isRecyclerScrollable()323 private boolean isRecyclerScrollable() { 324 if (mLayoutManager == null || mRecyclerView.getAdapter() == null) { 325 return false; 326 } 327 return mLayoutManager.findLastCompletelyVisibleItemPosition() 328 < mRecyclerView.getAdapter().getItemCount() - 1; 329 } 330 finishWithResultCancelled()331 private void finishWithResultCancelled() { 332 setResult(RESULT_CANCELED); 333 finish(); 334 } 335 logRequestFailure()336 private void logRequestFailure() { 337 DevicePolicyEventLogger 338 .createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_FAILED) 339 .write(); 340 } 341 getNumberOfAuthenticationPolicyUris(AppUriAuthenticationPolicy policy)342 private String getNumberOfAuthenticationPolicyUris(AppUriAuthenticationPolicy policy) { 343 int numberOfUris = 0; 344 for (Map.Entry<String, Map<Uri, String>> appsToUris : 345 policy.getAppAndUriMappings().entrySet()) { 346 numberOfUris += appsToUris.getValue().size(); 347 } 348 return String.valueOf(numberOfUris); 349 } 350 getNumberOfAuthenticationPolicyApps(AppUriAuthenticationPolicy policy)351 private String getNumberOfAuthenticationPolicyApps(AppUriAuthenticationPolicy policy) { 352 return String.valueOf(policy.getAppAndUriMappings().size()); 353 } 354 355 } 356