• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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 android.app.Activity;
20 import android.app.admin.DevicePolicyManager;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.pm.UserInfo;
26 import android.content.res.Resources;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.Process;
30 import android.os.RemoteException;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.security.Credentials;
34 import android.security.KeyChain;
35 import android.security.KeyChain.KeyChainConnection;
36 import android.security.KeyStore;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.widget.Toast;
40 
41 import androidx.appcompat.app.AlertDialog;
42 import androidx.fragment.app.FragmentActivity;
43 
44 import com.android.internal.widget.LockPatternUtils;
45 import com.android.org.bouncycastle.asn1.ASN1InputStream;
46 import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
47 import com.android.settings.R;
48 import com.android.settings.password.ChooseLockSettingsHelper;
49 import com.android.settings.vpn2.VpnUtils;
50 
51 import java.io.ByteArrayInputStream;
52 import java.io.IOException;
53 
54 import sun.security.util.ObjectIdentifier;
55 import sun.security.x509.AlgorithmId;
56 
57 /**
58  * CredentialStorage handles resetting and installing keys into KeyStore.
59  */
60 public final class CredentialStorage extends FragmentActivity {
61 
62     private static final String TAG = "CredentialStorage";
63 
64     public static final String ACTION_INSTALL = "com.android.credentials.INSTALL";
65     public static final String ACTION_RESET = "com.android.credentials.RESET";
66 
67     // This is the minimum acceptable password quality.  If the current password quality is
68     // lower than this, keystore should not be activated.
69     public static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
70 
71     private static final int CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST = 1;
72 
73     private final KeyStore mKeyStore = KeyStore.getInstance();
74     private LockPatternUtils mUtils;
75 
76     /**
77      * When non-null, the bundle containing credentials to install.
78      */
79     private Bundle mInstallBundle;
80 
81     @Override
onCreate(Bundle savedState)82     protected void onCreate(Bundle savedState) {
83         super.onCreate(savedState);
84         mUtils = new LockPatternUtils(this);
85     }
86 
87     @Override
onResume()88     protected void onResume() {
89         super.onResume();
90 
91         final Intent intent = getIntent();
92         final String action = intent.getAction();
93         final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
94         if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) {
95             if (ACTION_RESET.equals(action)) {
96                 new ResetDialog();
97             } else {
98                 if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) {
99                     mInstallBundle = intent.getExtras();
100                 }
101                 handleInstall();
102             }
103         } else {
104             finish();
105         }
106     }
107 
108     /**
109      * Install credentials from mInstallBundle into Keystore.
110      */
handleInstall()111     private void handleInstall() {
112         // something already decided we are done, do not proceed
113         if (isFinishing()) {
114             return;
115         }
116         if (installIfAvailable()) {
117             finish();
118         }
119     }
120 
isHardwareBackedKey(byte[] keyData)121     private boolean isHardwareBackedKey(byte[] keyData) {
122         try {
123             final ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData));
124             final PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
125             final String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
126             final String algName = new AlgorithmId(new ObjectIdentifier(algOid)).getName();
127 
128             return KeyChain.isBoundKeyAlgorithm(algName);
129         } catch (IOException e) {
130             Log.e(TAG, "Failed to parse key data");
131             return false;
132         }
133     }
134 
135     /**
136      * Install credentials if available, otherwise do nothing.
137      *
138      * @return true if the installation is done and the activity should be finished, false if
139      * an asynchronous task is pending and will finish the activity when it's done.
140      */
installIfAvailable()141     private boolean installIfAvailable() {
142         if (mInstallBundle == null || mInstallBundle.isEmpty()) {
143             return true;
144         }
145 
146         final Bundle bundle = mInstallBundle;
147         mInstallBundle = null;
148 
149         final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF);
150 
151         if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) {
152             final int dstUserId = UserHandle.getUserId(uid);
153 
154             // Restrict install target to the wifi uid.
155             if (uid != Process.WIFI_UID) {
156                 Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs"
157                         + " may only target wifi uids");
158                 return true;
159             }
160 
161             final Intent installIntent = new Intent(ACTION_INSTALL)
162                     .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
163                     .putExtras(bundle);
164             startActivityAsUser(installIntent, new UserHandle(dstUserId));
165             return true;
166         }
167 
168         boolean shouldFinish = true;
169         if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) {
170             final String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME);
171             final byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA);
172 
173             if (!mKeyStore.importKey(key, value, uid, KeyStore.FLAG_NONE)) {
174                 Log.e(TAG, "Failed to install " + key + " as uid " + uid);
175                 return true;
176             }
177             // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However,
178             // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY
179             // to the key name when interfacing with KeyStore.
180             // This is generally a symptom of CredentialStorage and CredentialHelper relying
181             // on internal implementation details of KeyChain and imitating its functionality
182             // rather than delegating to KeyChain for the certificate installation.
183             if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) {
184                 new MarkKeyAsUserSelectable(
185                         key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute();
186                 shouldFinish = false;
187             }
188         }
189 
190         final int flags = KeyStore.FLAG_NONE;
191 
192         if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) {
193             final String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME);
194             final byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA);
195 
196             if (!mKeyStore.put(certName, certData, uid, flags)) {
197                 Log.e(TAG, "Failed to install " + certName + " as uid " + uid);
198                 return shouldFinish;
199             }
200         }
201 
202         if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) {
203             final String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME);
204             final byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA);
205 
206             if (!mKeyStore.put(caListName, caListData, uid, flags)) {
207                 Log.e(TAG, "Failed to install " + caListName + " as uid " + uid);
208                 return shouldFinish;
209             }
210         }
211 
212         // Send the broadcast.
213         final Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED);
214         sendBroadcast(broadcast);
215 
216         setResult(RESULT_OK);
217         return shouldFinish;
218     }
219 
220     /**
221      * Prompt for reset confirmation, resetting on confirmation, finishing otherwise.
222      */
223     private class ResetDialog
224             implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
225         private boolean mResetConfirmed;
226 
ResetDialog()227         private ResetDialog() {
228             final AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
229                     .setTitle(android.R.string.dialog_alert_title)
230                     .setMessage(R.string.credentials_reset_hint)
231                     .setPositiveButton(android.R.string.ok, this)
232                     .setNegativeButton(android.R.string.cancel, this)
233                     .create();
234             dialog.setOnDismissListener(this);
235             dialog.show();
236         }
237 
238         @Override
onClick(DialogInterface dialog, int button)239         public void onClick(DialogInterface dialog, int button) {
240             mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
241         }
242 
243         @Override
onDismiss(DialogInterface dialog)244         public void onDismiss(DialogInterface dialog) {
245             if (!mResetConfirmed) {
246                 finish();
247                 return;
248             }
249 
250             mResetConfirmed = false;
251             if (!mUtils.isSecure(UserHandle.myUserId())) {
252                 // This task will call finish() in the end.
253                 new ResetKeyStoreAndKeyChain().execute();
254             } else if (!confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) {
255                 Log.w(TAG, "Failed to launch credential confirmation for a secure user.");
256                 finish();
257             }
258             // Confirmation result will be handled in onActivityResult if needed.
259         }
260     }
261 
262     /**
263      * Background task to handle reset of both keystore and user installed CAs.
264      */
265     private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> {
266 
267         @Override
doInBackground(Void... unused)268         protected Boolean doInBackground(Void... unused) {
269 
270             // Clear all the users credentials could have been installed in for this user.
271             mUtils.resetKeyStore(UserHandle.myUserId());
272 
273             try {
274                 final KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this);
275                 try {
276                     return keyChainConnection.getService().reset();
277                 } catch (RemoteException e) {
278                     return false;
279                 } finally {
280                     keyChainConnection.close();
281                 }
282             } catch (InterruptedException e) {
283                 Thread.currentThread().interrupt();
284                 return false;
285             }
286         }
287 
288         @Override
onPostExecute(Boolean success)289         protected void onPostExecute(Boolean success) {
290             if (success) {
291                 Toast.makeText(CredentialStorage.this,
292                         R.string.credentials_erased, Toast.LENGTH_SHORT).show();
293                 clearLegacyVpnIfEstablished();
294             } else {
295                 Toast.makeText(CredentialStorage.this,
296                         R.string.credentials_not_erased, Toast.LENGTH_SHORT).show();
297             }
298             finish();
299         }
300     }
301 
clearLegacyVpnIfEstablished()302     private void clearLegacyVpnIfEstablished() {
303         final boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext());
304         if (isDone) {
305             Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected,
306                     Toast.LENGTH_SHORT).show();
307         }
308     }
309 
310     /**
311      * Background task to mark a given key alias as user-selectable, so that
312      * it can be selected by users from the Certificate Selection prompt.
313      */
314     private class MarkKeyAsUserSelectable extends AsyncTask<Void, Void, Boolean> {
315         final String mAlias;
316 
MarkKeyAsUserSelectable(String alias)317         MarkKeyAsUserSelectable(String alias) {
318             mAlias = alias;
319         }
320 
321         @Override
doInBackground(Void... unused)322         protected Boolean doInBackground(Void... unused) {
323             try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) {
324                 keyChainConnection.getService().setUserSelectable(mAlias, true);
325                 return true;
326             } catch (RemoteException e) {
327                 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable.");
328                 return false;
329             } catch (InterruptedException e) {
330                 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable.");
331                 Thread.currentThread().interrupt();
332                 return false;
333             }
334         }
335 
336         @Override
onPostExecute(Boolean result)337         protected void onPostExecute(Boolean result) {
338             Log.i(TAG, String.format("Marked alias %s as selectable, success? %s",
339                         mAlias, result));
340             CredentialStorage.this.finish();
341         }
342     }
343 
344     /**
345      * Check that the caller is either certinstaller or Settings running in a profile of this user.
346      */
checkCallerIsCertInstallerOrSelfInProfile()347     private boolean checkCallerIsCertInstallerOrSelfInProfile() {
348         if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) {
349             // CertInstaller is allowed to install credentials if it has the same signature as
350             // Settings package.
351             return getPackageManager().checkSignatures(
352                     getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH;
353         }
354 
355         final int launchedFromUserId;
356         try {
357             final int launchedFromUid = android.app.ActivityManager.getService()
358                     .getLaunchedFromUid(getActivityToken());
359             if (launchedFromUid == -1) {
360                 Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult");
361                 return false;
362             }
363             if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) {
364                 // Not the same app
365                 return false;
366             }
367             launchedFromUserId = UserHandle.getUserId(launchedFromUid);
368         } catch (RemoteException re) {
369             // Error talking to ActivityManager, just give up
370             return false;
371         }
372 
373         final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
374         final UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId);
375         // Caller is running in a profile of this user
376         return ((parentInfo != null) && (parentInfo.id == UserHandle.myUserId()));
377     }
378 
379     /**
380      * Confirm existing key guard, returning password via onActivityResult.
381      */
confirmKeyGuard(int requestCode)382     private boolean confirmKeyGuard(int requestCode) {
383         final Resources res = getResources();
384         return new ChooseLockSettingsHelper(this)
385                 .launchConfirmationActivity(requestCode,
386                         res.getText(R.string.credentials_title), true);
387     }
388 
389     @Override
onActivityResult(int requestCode, int resultCode, Intent data)390     public void onActivityResult(int requestCode, int resultCode, Intent data) {
391         super.onActivityResult(requestCode, resultCode, data);
392         if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) {
393             if (resultCode == Activity.RESULT_OK) {
394                 new ResetKeyStoreAndKeyChain().execute();
395                 return;
396             }
397             // failed confirmation, bail
398             finish();
399         }
400     }
401 }
402