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