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