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