1/* 2 * Copyright (C) 2017 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 17package com.android.internal.telephony; 18 19import static android.preference.PreferenceManager.getDefaultSharedPreferences; 20 21import static java.nio.charset.StandardCharsets.UTF_8; 22 23import android.app.AlarmManager; 24import android.app.DownloadManager; 25import android.app.PendingIntent; 26import android.content.BroadcastReceiver; 27import android.content.Context; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.SharedPreferences; 31import android.database.Cursor; 32import android.net.Uri; 33import android.os.PersistableBundle; 34import android.telephony.CarrierConfigManager; 35import android.telephony.ImsiEncryptionInfo; 36import android.telephony.SubscriptionManager; 37import android.telephony.TelephonyManager; 38import android.text.TextUtils; 39import android.util.Log; 40import android.util.Pair; 41 42import com.android.internal.annotations.VisibleForTesting; 43import com.android.org.bouncycastle.util.io.pem.PemReader; 44 45import org.json.JSONArray; 46import org.json.JSONException; 47import org.json.JSONObject; 48 49import java.io.BufferedReader; 50import java.io.ByteArrayInputStream; 51import java.io.FileInputStream; 52import java.io.IOException; 53import java.io.InputStream; 54import java.io.InputStreamReader; 55import java.io.Reader; 56import java.security.PublicKey; 57import java.security.cert.CertificateFactory; 58import java.security.cert.X509Certificate; 59import java.util.Date; 60import java.util.zip.GZIPInputStream; 61 62/** 63 * This class contains logic to get Certificates and keep them current. 64 * The class will be instantiated by various Phone implementations. 65 */ 66public class CarrierKeyDownloadManager { 67 private static final String LOG_TAG = "CarrierKeyDownloadManager"; 68 69 private static final String MCC_MNC_PREF_TAG = "CARRIER_KEY_DM_MCC_MNC"; 70 71 private static final int DAY_IN_MILLIS = 24 * 3600 * 1000; 72 73 // Start trying to renew the cert X days before it expires. 74 private static final int DEFAULT_RENEWAL_WINDOW_DAYS = 7; 75 76 /* Intent for downloading the public key */ 77 private static final String INTENT_KEY_RENEWAL_ALARM_PREFIX = 78 "com.android.internal.telephony.carrier_key_download_alarm"; 79 80 @VisibleForTesting 81 public int mKeyAvailability = 0; 82 83 public static final String MNC = "MNC"; 84 public static final String MCC = "MCC"; 85 private static final String SEPARATOR = ":"; 86 87 private static final String JSON_CERTIFICATE = "certificate"; 88 // This is a hack to accommodate certain Carriers who insists on using the public-key 89 // field to store the certificate. We'll just use which-ever is not null. 90 private static final String JSON_CERTIFICATE_ALTERNATE = "public-key"; 91 private static final String JSON_TYPE = "key-type"; 92 private static final String JSON_IDENTIFIER = "key-identifier"; 93 private static final String JSON_CARRIER_KEYS = "carrier-keys"; 94 private static final String JSON_TYPE_VALUE_WLAN = "WLAN"; 95 private static final String JSON_TYPE_VALUE_EPDG = "EPDG"; 96 97 98 private static final int[] CARRIER_KEY_TYPES = {TelephonyManager.KEY_TYPE_EPDG, 99 TelephonyManager.KEY_TYPE_WLAN}; 100 private static final int UNINITIALIZED_KEY_TYPE = -1; 101 102 private final Phone mPhone; 103 private final Context mContext; 104 public final DownloadManager mDownloadManager; 105 private String mURL; 106 107 public CarrierKeyDownloadManager(Phone phone) { 108 mPhone = phone; 109 mContext = phone.getContext(); 110 IntentFilter filter = new IntentFilter(); 111 filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); 112 filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE); 113 filter.addAction(INTENT_KEY_RENEWAL_ALARM_PREFIX + mPhone.getPhoneId()); 114 mContext.registerReceiver(mBroadcastReceiver, filter, null, phone); 115 mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); 116 } 117 118 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 119 @Override 120 public void onReceive(Context context, Intent intent) { 121 String action = intent.getAction(); 122 int slotId = mPhone.getPhoneId(); 123 if (action.equals(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId)) { 124 Log.d(LOG_TAG, "Handling key renewal alarm: " + action); 125 handleAlarmOrConfigChange(); 126 } else if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) { 127 if (slotId == intent.getIntExtra(PhoneConstants.PHONE_KEY, 128 SubscriptionManager.INVALID_SIM_SLOT_INDEX)) { 129 Log.d(LOG_TAG, "Carrier Config changed: " + action); 130 handleAlarmOrConfigChange(); 131 } 132 } else if (action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) { 133 Log.d(LOG_TAG, "Download Complete"); 134 long carrierKeyDownloadIdentifier = 135 intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); 136 String mccMnc = getMccMncSetFromPref(); 137 if (isValidDownload(mccMnc)) { 138 onDownloadComplete(carrierKeyDownloadIdentifier, mccMnc); 139 onPostDownloadProcessing(carrierKeyDownloadIdentifier); 140 } 141 } 142 } 143 }; 144 145 private void onPostDownloadProcessing(long carrierKeyDownloadIdentifier) { 146 resetRenewalAlarm(); 147 cleanupDownloadPreferences(carrierKeyDownloadIdentifier); 148 } 149 150 private void handleAlarmOrConfigChange() { 151 if (carrierUsesKeys()) { 152 if (areCarrierKeysAbsentOrExpiring()) { 153 boolean downloadStartedSuccessfully = downloadKey(); 154 // if the download was attemped, but not started successfully, and if carriers uses 155 // keys, we'll still want to renew the alarms, and try downloading the key a day 156 // later. 157 if (!downloadStartedSuccessfully) { 158 resetRenewalAlarm(); 159 } 160 } else { 161 return; 162 } 163 } else { 164 // delete any existing alarms. 165 cleanupRenewalAlarms(); 166 } 167 } 168 169 private void cleanupDownloadPreferences(long carrierKeyDownloadIdentifier) { 170 Log.d(LOG_TAG, "Cleaning up download preferences: " + carrierKeyDownloadIdentifier); 171 SharedPreferences.Editor editor = getDefaultSharedPreferences(mContext).edit(); 172 editor.remove(String.valueOf(carrierKeyDownloadIdentifier)); 173 editor.commit(); 174 } 175 176 private void cleanupRenewalAlarms() { 177 Log.d(LOG_TAG, "Cleaning up existing renewal alarms"); 178 int slotId = mPhone.getPhoneId(); 179 Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId); 180 PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent, 181 PendingIntent.FLAG_UPDATE_CURRENT); 182 AlarmManager alarmManager = 183 (AlarmManager) mContext.getSystemService(mContext.ALARM_SERVICE); 184 alarmManager.cancel(carrierKeyDownloadIntent); 185 } 186 187 /** 188 * this method returns the date to be used to decide on when to start downloading the key. 189 * from the carrier. 190 **/ 191 @VisibleForTesting 192 public long getExpirationDate() { 193 long minExpirationDate = Long.MAX_VALUE; 194 for (int key_type : CARRIER_KEY_TYPES) { 195 if (!isKeyEnabled(key_type)) { 196 continue; 197 } 198 ImsiEncryptionInfo imsiEncryptionInfo = 199 mPhone.getCarrierInfoForImsiEncryption(key_type); 200 if (imsiEncryptionInfo != null && imsiEncryptionInfo.getExpirationTime() != null) { 201 if (minExpirationDate > imsiEncryptionInfo.getExpirationTime().getTime()) { 202 minExpirationDate = imsiEncryptionInfo.getExpirationTime().getTime(); 203 } 204 } 205 } 206 207 // if there are no keys, or expiration date is in the past, or within 7 days, then we 208 // set the alarm to run in a day. Else, we'll set the alarm to run 7 days prior to 209 // expiration. 210 if (minExpirationDate == Long.MAX_VALUE || (minExpirationDate 211 < System.currentTimeMillis() + DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) { 212 minExpirationDate = System.currentTimeMillis() + DAY_IN_MILLIS; 213 } else { 214 minExpirationDate = minExpirationDate - DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS; 215 } 216 return minExpirationDate; 217 } 218 219 /** 220 * this method resets the alarm. Starts by cleaning up the existing alarms. 221 * We look at the earliest expiration date, and setup an alarms X days prior. 222 * If the expiration date is in the past, we'll setup an alarm to run the next day. This 223 * could happen if the download has failed. 224 **/ 225 @VisibleForTesting 226 public void resetRenewalAlarm() { 227 cleanupRenewalAlarms(); 228 int slotId = mPhone.getPhoneId(); 229 long minExpirationDate = getExpirationDate(); 230 Log.d(LOG_TAG, "minExpirationDate: " + new Date(minExpirationDate)); 231 final AlarmManager alarmManager = (AlarmManager) mContext.getSystemService( 232 Context.ALARM_SERVICE); 233 Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId); 234 PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent, 235 PendingIntent.FLAG_UPDATE_CURRENT); 236 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, minExpirationDate, 237 carrierKeyDownloadIntent); 238 Log.d(LOG_TAG, "setRenewelAlarm: action=" + intent.getAction() + " time=" 239 + new Date(minExpirationDate)); 240 } 241 242 private String getMccMncSetFromPref() { 243 // check if this is a download that we had created. We do this by checking if the 244 // downloadId is stored in the shared prefs. 245 int slotId = mPhone.getPhoneId(); 246 SharedPreferences preferences = getDefaultSharedPreferences(mContext); 247 return preferences.getString(MCC_MNC_PREF_TAG + slotId, null); 248 } 249 250 /** 251 * Returns the sim operator. 252 **/ 253 @VisibleForTesting 254 public String getSimOperator() { 255 final TelephonyManager telephonyManager = 256 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 257 return telephonyManager.getSimOperator(mPhone.getSubId()); 258 } 259 260 /** 261 * checks if the download was sent by this particular instance. We do this by including the 262 * slot id in the key. If no value is found, we know that the download was not for this 263 * instance of the phone. 264 **/ 265 @VisibleForTesting 266 public boolean isValidDownload(String mccMnc) { 267 String mccCurrent = ""; 268 String mncCurrent = ""; 269 String mccSource = ""; 270 String mncSource = ""; 271 272 String simOperator = getSimOperator(); 273 if (TextUtils.isEmpty(simOperator) || TextUtils.isEmpty(mccMnc)) { 274 Log.e(LOG_TAG, "simOperator or mcc/mnc is empty"); 275 return false; 276 } 277 278 String[] splitValue = mccMnc.split(SEPARATOR); 279 mccSource = splitValue[0]; 280 mncSource = splitValue[1]; 281 Log.d(LOG_TAG, "values from sharedPrefs mcc, mnc: " + mccSource + "," + mncSource); 282 283 mccCurrent = simOperator.substring(0, 3); 284 mncCurrent = simOperator.substring(3); 285 Log.d(LOG_TAG, "using values for mcc, mnc: " + mccCurrent + "," + mncCurrent); 286 287 if (TextUtils.equals(mncSource, mncCurrent) && TextUtils.equals(mccSource, mccCurrent)) { 288 return true; 289 } 290 return false; 291 } 292 293 /** 294 * This method will try to parse the downloaded information, and persist it in the database. 295 **/ 296 private void onDownloadComplete(long carrierKeyDownloadIdentifier, String mccMnc) { 297 Log.d(LOG_TAG, "onDownloadComplete: " + carrierKeyDownloadIdentifier); 298 String jsonStr; 299 DownloadManager.Query query = new DownloadManager.Query(); 300 query.setFilterById(carrierKeyDownloadIdentifier); 301 Cursor cursor = mDownloadManager.query(query); 302 InputStream source = null; 303 304 if (cursor == null) { 305 return; 306 } 307 if (cursor.moveToFirst()) { 308 int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); 309 if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) { 310 try { 311 source = new FileInputStream( 312 mDownloadManager.openDownloadedFile(carrierKeyDownloadIdentifier) 313 .getFileDescriptor()); 314 jsonStr = convertToString(source); 315 parseJsonAndPersistKey(jsonStr, mccMnc); 316 } catch (Exception e) { 317 Log.e(LOG_TAG, "Error in download:" + carrierKeyDownloadIdentifier 318 + ". " + e); 319 } finally { 320 mDownloadManager.remove(carrierKeyDownloadIdentifier); 321 try { 322 source.close(); 323 } catch (IOException e) { 324 e.printStackTrace(); 325 } 326 } 327 } 328 Log.d(LOG_TAG, "Completed downloading keys"); 329 } 330 cursor.close(); 331 return; 332 } 333 334 /** 335 * This method checks if the carrier requires key. We'll read the carrier config to make that 336 * determination. 337 * @return boolean returns true if carrier requires keys, else false. 338 **/ 339 private boolean carrierUsesKeys() { 340 CarrierConfigManager carrierConfigManager = (CarrierConfigManager) 341 mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); 342 if (carrierConfigManager == null) { 343 return false; 344 } 345 int subId = mPhone.getSubId(); 346 PersistableBundle b = carrierConfigManager.getConfigForSubId(subId); 347 if (b == null) { 348 return false; 349 } 350 mKeyAvailability = b.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT); 351 mURL = b.getString(CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING); 352 if (TextUtils.isEmpty(mURL) || mKeyAvailability == 0) { 353 Log.d(LOG_TAG, "Carrier not enabled or invalid values"); 354 return false; 355 } 356 for (int key_type : CARRIER_KEY_TYPES) { 357 if (isKeyEnabled(key_type)) { 358 return true; 359 } 360 } 361 return false; 362 } 363 364 private static String convertToString(InputStream is) { 365 try { 366 // The current implementation at certain Carriers has the data gzipped, which requires 367 // us to unzip the contents. Longer term, we want to add a flag in carrier config which 368 // determines if the data needs to be zipped or not. 369 GZIPInputStream gunzip = new GZIPInputStream(is); 370 BufferedReader reader = new BufferedReader(new InputStreamReader(gunzip, UTF_8)); 371 StringBuilder sb = new StringBuilder(); 372 373 String line; 374 while ((line = reader.readLine()) != null) { 375 sb.append(line).append('\n'); 376 } 377 return sb.toString(); 378 } catch (IOException e) { 379 e.printStackTrace(); 380 } 381 return null; 382 } 383 384 /** 385 * Converts the string into a json object to retreive the nodes. The Json should have 3 nodes, 386 * including the Carrier public key, the key type and the key identifier. Once the nodes have 387 * been extracted, they get persisted to the database. Sample: 388 * "carrier-keys": [ { "certificate": "", 389 * "key-type": "WLAN", 390 * "key-identifier": "" 391 * } ] 392 * @param jsonStr the json string. 393 * @param mccMnc contains the mcc, mnc. 394 */ 395 @VisibleForTesting 396 public void parseJsonAndPersistKey(String jsonStr, String mccMnc) { 397 if (TextUtils.isEmpty(jsonStr) || TextUtils.isEmpty(mccMnc)) { 398 Log.e(LOG_TAG, "jsonStr or mcc, mnc: is empty"); 399 return; 400 } 401 PemReader reader = null; 402 try { 403 String mcc = ""; 404 String mnc = ""; 405 String[] splitValue = mccMnc.split(SEPARATOR); 406 mcc = splitValue[0]; 407 mnc = splitValue[1]; 408 JSONObject jsonObj = new JSONObject(jsonStr); 409 JSONArray keys = jsonObj.getJSONArray(JSON_CARRIER_KEYS); 410 for (int i = 0; i < keys.length(); i++) { 411 JSONObject key = keys.getJSONObject(i); 412 // This is a hack to accommodate certain carriers who insist on using the public-key 413 // field to store the certificate. We'll just use which-ever is not null. 414 String cert = null; 415 if (key.has(JSON_CERTIFICATE)) { 416 cert = key.getString(JSON_CERTIFICATE); 417 } else { 418 cert = key.getString(JSON_CERTIFICATE_ALTERNATE); 419 } 420 String typeString = key.getString(JSON_TYPE); 421 int type = UNINITIALIZED_KEY_TYPE; 422 if (typeString.equals(JSON_TYPE_VALUE_WLAN)) { 423 type = TelephonyManager.KEY_TYPE_WLAN; 424 } else if (typeString.equals(JSON_TYPE_VALUE_EPDG)) { 425 type = TelephonyManager.KEY_TYPE_EPDG; 426 } 427 String identifier = key.getString(JSON_IDENTIFIER); 428 ByteArrayInputStream inStream = new ByteArrayInputStream(cert.getBytes()); 429 Reader fReader = new BufferedReader(new InputStreamReader(inStream)); 430 reader = new PemReader(fReader); 431 Pair<PublicKey, Long> keyInfo = 432 getKeyInformation(reader.readPemObject().getContent()); 433 reader.close(); 434 savePublicKey(keyInfo.first, type, identifier, keyInfo.second, mcc, mnc); 435 } 436 } catch (final JSONException e) { 437 Log.e(LOG_TAG, "Json parsing error: " + e.getMessage()); 438 } catch (final Exception e) { 439 Log.e(LOG_TAG, "Exception getting certificate: " + e); 440 } finally { 441 try { 442 if (reader != null) { 443 reader.close(); 444 } 445 } catch (final Exception e) { 446 Log.e(LOG_TAG, "Exception getting certificate: " + e); 447 } 448 } 449 } 450 451 /** 452 * introspects the mKeyAvailability bitmask 453 * @return true if the digit at position k is 1, else false. 454 */ 455 @VisibleForTesting 456 public boolean isKeyEnabled(int keyType) { 457 //since keytype has values of 1, 2.... we need to subtract 1 from the keytype. 458 int returnValue = (mKeyAvailability >> (keyType - 1)) & 1; 459 return (returnValue == 1) ? true : false; 460 } 461 462 /** 463 * Checks whether is the keys are absent or close to expiration. Returns true, if either of 464 * those conditions are true. 465 * @return boolean returns true when keys are absent or close to expiration, else false. 466 */ 467 @VisibleForTesting 468 public boolean areCarrierKeysAbsentOrExpiring() { 469 for (int key_type : CARRIER_KEY_TYPES) { 470 if (!isKeyEnabled(key_type)) { 471 continue; 472 } 473 ImsiEncryptionInfo imsiEncryptionInfo = 474 mPhone.getCarrierInfoForImsiEncryption(key_type); 475 if (imsiEncryptionInfo == null) { 476 Log.d(LOG_TAG, "Key not found for: " + key_type); 477 return true; 478 } 479 Date imsiDate = imsiEncryptionInfo.getExpirationTime(); 480 long timeToExpire = imsiDate.getTime() - System.currentTimeMillis(); 481 return (timeToExpire < DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS) ? true : false; 482 } 483 return false; 484 } 485 486 private boolean downloadKey() { 487 Log.d(LOG_TAG, "starting download from: " + mURL); 488 String mcc = ""; 489 String mnc = ""; 490 String simOperator = getSimOperator(); 491 492 if (!TextUtils.isEmpty(simOperator)) { 493 mcc = simOperator.substring(0, 3); 494 mnc = simOperator.substring(3); 495 Log.d(LOG_TAG, "using values for mcc, mnc: " + mcc + "," + mnc); 496 } else { 497 Log.e(LOG_TAG, "mcc, mnc: is empty"); 498 return false; 499 } 500 try { 501 DownloadManager.Request request = new DownloadManager.Request(Uri.parse(mURL)); 502 request.setAllowedOverMetered(false); 503 request.setVisibleInDownloadsUi(false); 504 Long carrierKeyDownloadRequestId = mDownloadManager.enqueue(request); 505 SharedPreferences.Editor editor = getDefaultSharedPreferences(mContext).edit(); 506 507 String mccMnc = mcc + SEPARATOR + mnc; 508 int slotId = mPhone.getPhoneId(); 509 Log.d(LOG_TAG, "storing values in sharedpref mcc, mnc, days: " + mcc + "," + mnc 510 + "," + carrierKeyDownloadRequestId); 511 editor.putString(MCC_MNC_PREF_TAG + slotId, mccMnc); 512 editor.commit(); 513 } catch (Exception e) { 514 Log.e(LOG_TAG, "exception trying to dowload key from url: " + mURL); 515 return false; 516 } 517 return true; 518 } 519 520 /** 521 * Save the public key 522 * @param certificate certificate that contains the public key. 523 * @return Pair containing the Public Key and the expiration date. 524 **/ 525 @VisibleForTesting 526 public static Pair<PublicKey, Long> getKeyInformation(byte[] certificate) throws Exception { 527 InputStream inStream = new ByteArrayInputStream(certificate); 528 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 529 X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream); 530 Pair<PublicKey, Long> keyInformation = 531 new Pair(cert.getPublicKey(), cert.getNotAfter().getTime()); 532 return keyInformation; 533 } 534 535 /** 536 * Save the public key 537 * @param publicKey public key. 538 * @param type key-type. 539 * @param identifier which is an opaque string. 540 * @param expirationDate expiration date of the key. 541 * @param mcc 542 * @param mnc 543 **/ 544 @VisibleForTesting 545 public void savePublicKey(PublicKey publicKey, int type, String identifier, long expirationDate, 546 String mcc, String mnc) { 547 ImsiEncryptionInfo imsiEncryptionInfo = new ImsiEncryptionInfo(mcc, mnc, type, identifier, 548 publicKey, new Date(expirationDate)); 549 mPhone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo); 550 } 551} 552