• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.certinstaller;
18 
19 import android.app.KeyguardManager;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.net.Uri;
23 import android.os.Bundle;
24 import android.os.UserManager;
25 import android.preference.PreferenceActivity;
26 import android.provider.DocumentsContract;
27 import android.security.Credentials;
28 import android.security.KeyChain;
29 import android.util.Log;
30 import android.widget.Toast;
31 
32 import libcore.io.IoUtils;
33 
34 import java.io.BufferedInputStream;
35 import java.io.ByteArrayOutputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.util.HashMap;
39 import java.util.Map;
40 
41 /**
42  * The main class for installing certificates to the system keystore. It reacts
43  * to the public {@link Credentials#INSTALL_ACTION} intent.
44  */
45 public class CertInstallerMain extends PreferenceActivity {
46     private static final String TAG = "CertInstaller";
47 
48     private static final int REQUEST_INSTALL = 1;
49     private static final int REQUEST_OPEN_DOCUMENT = 2;
50     private static final int REQUEST_CONFIRM_CREDENTIALS = 3;
51 
52     private static final String INSTALL_CERT_AS_USER_CLASS = ".InstallCertAsUser";
53 
54     public static final String WIFI_CONFIG = "wifi-config";
55     public static final String WIFI_CONFIG_DATA = "wifi-config-data";
56     public static final String WIFI_CONFIG_FILE = "wifi-config-file";
57 
58     private static Map<String,String> MIME_MAPPINGS = new HashMap<>();
59 
60     static {
61             MIME_MAPPINGS.put("application/x-x509-ca-cert", KeyChain.EXTRA_CERTIFICATE);
62             MIME_MAPPINGS.put("application/x-x509-user-cert", KeyChain.EXTRA_CERTIFICATE);
63             MIME_MAPPINGS.put("application/x-x509-server-cert", KeyChain.EXTRA_CERTIFICATE);
64             MIME_MAPPINGS.put("application/x-pem-file", KeyChain.EXTRA_CERTIFICATE);
65             MIME_MAPPINGS.put("application/pkix-cert", KeyChain.EXTRA_CERTIFICATE);
66             MIME_MAPPINGS.put("application/x-pkcs12", KeyChain.EXTRA_PKCS12);
67             MIME_MAPPINGS.put("application/x-wifi-config", WIFI_CONFIG);
68     }
69 
70     @Override
onCreate(Bundle savedInstanceState)71     protected void onCreate(Bundle savedInstanceState) {
72         super.onCreate(savedInstanceState);
73 
74         setResult(RESULT_CANCELED);
75 
76         UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
77         if (userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)
78                 || userManager.isGuestUser()) {
79             finish();
80             return;
81         }
82 
83         final Intent intent = getIntent();
84         final String action = intent.getAction();
85 
86         if (Credentials.INSTALL_ACTION.equals(action)
87                 || Credentials.INSTALL_AS_USER_ACTION.equals(action)) {
88             Bundle bundle = intent.getExtras();
89 
90             /*
91              * There is a special INSTALL_AS_USER action that this activity is
92              * aliased to, but you have to have a permission to call it. If the
93              * caller got here any other way, remove the extra that we allow in
94              * that INSTALL_AS_USER path.
95              */
96             String calledClass = intent.getComponent().getClassName();
97             String installAsUserClassName = getPackageName() + INSTALL_CERT_AS_USER_CLASS;
98             if (bundle != null && !installAsUserClassName.equals(calledClass)) {
99                 bundle.remove(Credentials.EXTRA_INSTALL_AS_UID);
100             }
101 
102             // If bundle is empty of any actual credentials, ask user to open.
103             // Otherwise, pass extras to CertInstaller to install those credentials.
104             // Either way, we use KeyChain.EXTRA_NAME as the default name if available.
105             if (nullOrEmptyBundle(bundle) || bundleContainsNameOnly(bundle)
106                     || bundleContainsInstallAsUidOnly(bundle)
107                     || bundleContainsExtraCertificateUsageOnly(bundle)) {
108 
109                 // Confirm credentials if there's only a CA certificate
110                 if (installingCaCertificate(bundle)) {
111                     confirmDeviceCredential();
112                 } else {
113                     startOpenDocumentActivity();
114                 }
115             } else {
116                 startInstallActivity(intent);
117             }
118         } else if (Intent.ACTION_VIEW.equals(action)) {
119             startInstallActivity(intent.getType(), intent.getData());
120         }
121     }
122 
nullOrEmptyBundle(Bundle bundle)123     private boolean nullOrEmptyBundle(Bundle bundle) {
124         return bundle == null || bundle.isEmpty();
125     }
126 
bundleContainsNameOnly(Bundle bundle)127     private boolean bundleContainsNameOnly(Bundle bundle) {
128         return bundle.size() == 1 && bundle.containsKey(KeyChain.EXTRA_NAME);
129     }
130 
bundleContainsInstallAsUidOnly(Bundle bundle)131     private boolean bundleContainsInstallAsUidOnly(Bundle bundle) {
132         return bundle.size() == 1 && bundle.containsKey(Credentials.EXTRA_INSTALL_AS_UID);
133     }
134 
bundleContainsExtraCertificateUsageOnly(Bundle bundle)135     private boolean bundleContainsExtraCertificateUsageOnly(Bundle bundle) {
136         return bundle.size() == 1 && bundle.containsKey(Credentials.EXTRA_CERTIFICATE_USAGE);
137     }
138 
installingCaCertificate(Bundle bundle)139     private boolean installingCaCertificate(Bundle bundle) {
140         return bundle != null && bundle.size() == 1 && Credentials.CERTIFICATE_USAGE_CA.equals(
141                 bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE));
142     }
143 
confirmDeviceCredential()144     private void confirmDeviceCredential() {
145         KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
146         Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null,
147                 null);
148         if (intent == null) { // No screenlock
149             startOpenDocumentActivity();
150         } else {
151             startActivityForResult(intent, REQUEST_CONFIRM_CREDENTIALS);
152         }
153     }
154 
155     // The maximum amount of data to read into memory before aborting.
156     // Without a limit, a sufficiently-large file will run us out of memory.  A
157     // typical certificate or WiFi config is under 10k, so 10MiB should be more
158     // than sufficient.  See b/32320490.
159     private static final int READ_LIMIT = 10 * 1024 * 1024;
160 
161     /**
162      * Reads the given InputStream until EOF or more than READ_LIMIT bytes have
163      * been read, whichever happens first.  If the maximum limit is reached, throws
164      * IOException.
165      */
readWithLimit(InputStream in)166     private static byte[] readWithLimit(InputStream in) throws IOException {
167         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
168         byte[] buffer = new byte[1024];
169         int bytesRead = 0;
170         int count;
171         while ((count = in.read(buffer)) != -1) {
172             bytes.write(buffer, 0, count);
173             bytesRead += count;
174             if (bytesRead > READ_LIMIT) {
175                 throw new IOException("Data file exceeded maximum size.");
176             }
177         }
178         return bytes.toByteArray();
179     }
180 
startInstallActivity(Intent intent)181     private void startInstallActivity(Intent intent) {
182         final Intent installIntent = new Intent(this, CertInstaller.class);
183         if (intent.getExtras() != null && intent.getExtras().getString(Intent.EXTRA_REFERRER)
184                 != null) {
185             Log.v(TAG, String.format(
186                     "Removing referrer extra with value %s which was not meant to be included",
187                     intent.getBundleExtra(Intent.EXTRA_REFERRER)));
188             intent.removeExtra(Intent.EXTRA_REFERRER);
189         }
190         installIntent.putExtras(intent);
191 
192         // The referrer is passed as an extra because the launched-from package needs to be
193         // obtained here and not in the CertInstaller.
194         // It is also safe to add the referrer as an extra because the CertInstaller activity
195         // is not exported, which means it cannot be called from other apps.
196         installIntent.putExtra(Intent.EXTRA_REFERRER, getLaunchedFromPackage());
197         startActivityForResult(installIntent, REQUEST_INSTALL);
198     }
199 
startInstallActivity(String mimeType, Uri uri)200     private void startInstallActivity(String mimeType, Uri uri) {
201         if (mimeType == null) {
202             mimeType = getContentResolver().getType(uri);
203         }
204 
205         String target = MIME_MAPPINGS.get(mimeType);
206         if (target == null) {
207             throw new IllegalArgumentException("Unknown MIME type: " + mimeType);
208         }
209 
210         if (WIFI_CONFIG.equals(target)) {
211             startWifiInstallActivity(mimeType, uri);
212         }
213         else {
214             InputStream in = null;
215             try {
216                 in = getContentResolver().openInputStream(uri);
217 
218                 final byte[] raw = readWithLimit(in);
219 
220                 Intent intent = getIntent();
221                 intent.putExtra(target, raw);
222                 startInstallActivity(intent);
223             } catch (IOException e) {
224                 Log.e(TAG, "Failed to read certificate: " + e);
225                 Toast.makeText(this, R.string.cert_read_error, Toast.LENGTH_LONG).show();
226             } finally {
227                 IoUtils.closeQuietly(in);
228             }
229         }
230     }
231 
startWifiInstallActivity(String mimeType, Uri uri)232     private void startWifiInstallActivity(String mimeType, Uri uri) {
233         Intent intent = new Intent(this, WiFiInstaller.class);
234         try (BufferedInputStream in =
235                      new BufferedInputStream(getContentResolver().openInputStream(uri))) {
236             byte[] data = readWithLimit(in);
237             intent.putExtra(WIFI_CONFIG_FILE, uri.toString());
238             intent.putExtra(WIFI_CONFIG_DATA, data);
239             intent.putExtra(WIFI_CONFIG, mimeType);
240             startActivityForResult(intent, REQUEST_INSTALL);
241         } catch (IOException e) {
242             Log.e(TAG, "Failed to read wifi config: " + e);
243             Toast.makeText(this, R.string.cert_read_error, Toast.LENGTH_LONG).show();
244         }
245     }
246 
startOpenDocumentActivity()247     private void startOpenDocumentActivity() {
248         final String[] mimeTypes = MIME_MAPPINGS.keySet().toArray(new String[0]);
249         final Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
250         openIntent.setType("*/*");
251         openIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
252         openIntent.putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, true);
253         startActivityForResult(openIntent, REQUEST_OPEN_DOCUMENT);
254     }
255 
256     @Override
onActivityResult(int requestCode, int resultCode, Intent data)257     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
258         switch (requestCode) {
259             case REQUEST_INSTALL:
260                 setResult(resultCode);
261                 finish();
262                 break;
263             case REQUEST_OPEN_DOCUMENT:
264                 if (resultCode == RESULT_OK) {
265                     startInstallActivity(null, data.getData());
266                 } else {
267                     finish();
268                 }
269                 break;
270             case REQUEST_CONFIRM_CREDENTIALS:
271                 if (resultCode == RESULT_OK) {
272                     startOpenDocumentActivity();
273                     return;
274                 }
275                 // Failed to confirm credentials, do nothing.
276                 finish();
277                 break;
278             default:
279                 Log.w(TAG, "unknown request code: " + requestCode);
280                 break;
281         }
282     }
283 }
284