/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony; import static java.nio.charset.StandardCharsets.UTF_8; import android.annotation.NonNull; import android.app.AlarmManager; import android.app.DownloadManager; import android.app.KeyguardManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.Network; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.os.PersistableBundle; import android.os.UserManager; import android.telephony.CarrierConfigManager; import android.telephony.ImsiEncryptionInfo; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.flags.Flags; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.security.PublicKey; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Date; import java.util.List; import java.util.Random; import java.util.zip.GZIPInputStream; import java.util.zip.ZipException; /** * This class contains logic to get Certificates and keep them current. * The class will be instantiated by various Phone implementations. */ public class CarrierKeyDownloadManager extends Handler { private static final String LOG_TAG = "CarrierKeyDownloadManager"; private static final String CERT_BEGIN_STRING = "-----BEGIN CERTIFICATE-----"; private static final String CERT_END_STRING = "-----END CERTIFICATE-----"; private static final int DAY_IN_MILLIS = 24 * 3600 * 1000; // Create a window prior to the key expiration, during which the cert will be // downloaded. Defines the start date of that window. So if the key expires on // Dec 21st, the start of the renewal window will be Dec 1st. private static final int START_RENEWAL_WINDOW_DAYS = 21; // This will define the end date of the window. private static final int END_RENEWAL_WINDOW_DAYS = 7; /* Intent for downloading the public key */ private static final String INTENT_KEY_RENEWAL_ALARM_PREFIX = "com.android.internal.telephony.carrier_key_download_alarm"; @VisibleForTesting public int mKeyAvailability = 0; private static final String JSON_CERTIFICATE = "certificate"; private static final String JSON_CERTIFICATE_ALTERNATE = "public-key"; private static final String JSON_TYPE = "key-type"; private static final String JSON_IDENTIFIER = "key-identifier"; private static final String JSON_CARRIER_KEYS = "carrier-keys"; private static final String JSON_TYPE_VALUE_WLAN = "WLAN"; private static final String JSON_TYPE_VALUE_EPDG = "EPDG"; private static final int EVENT_ALARM_OR_CONFIG_CHANGE = 0; private static final int EVENT_DOWNLOAD_COMPLETE = 1; private static final int EVENT_NETWORK_AVAILABLE = 2; private static final int EVENT_SCREEN_UNLOCKED = 3; private static final int[] CARRIER_KEY_TYPES = {TelephonyManager.KEY_TYPE_EPDG, TelephonyManager.KEY_TYPE_WLAN}; private final Phone mPhone; private final Context mContext; public final DownloadManager mDownloadManager; private String mURL; private boolean mAllowedOverMeteredNetwork = false; private boolean mDeleteOldKeyAfterDownload = false; private boolean mIsRequiredToHandleUnlock; private final TelephonyManager mTelephonyManager; private UserManager mUserManager; @VisibleForTesting public String mMccMncForDownload = ""; public int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; @VisibleForTesting public long mDownloadId; private DefaultNetworkCallback mDefaultNetworkCallback; private ConnectivityManager mConnectivityManager; private KeyguardManager mKeyguardManager; public CarrierKeyDownloadManager(Phone phone) { mPhone = phone; mContext = phone.getContext(); IntentFilter filter = new IntentFilter(); filter.addAction(INTENT_KEY_RENEWAL_ALARM_PREFIX); filter.addAction(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD); if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { filter.addAction(Intent.ACTION_USER_UNLOCKED); } mContext.registerReceiver(mBroadcastReceiver, filter, null, phone); mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); mTelephonyManager = mContext.getSystemService(TelephonyManager.class) .createForSubscriptionId(mPhone.getSubId()); if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { mKeyguardManager = mContext.getSystemService(KeyguardManager.class); } else { mUserManager = mContext.getSystemService(UserManager.class); } CarrierConfigManager carrierConfigManager = mContext.getSystemService( CarrierConfigManager.class); // Callback which directly handle config change should be executed on handler thread carrierConfigManager.registerCarrierConfigChangeListener(this::post, (slotIndex, subId, carrierId, specificCarrierId) -> { if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { logd("CarrierConfig changed slotIndex = " + slotIndex + " subId = " + subId + " CarrierId = " + carrierId + " phoneId = " + mPhone.getPhoneId()); // Below checks are necessary to optimise the logic. if ((slotIndex == mPhone.getPhoneId()) && (carrierId > 0 || !TextUtils.isEmpty( mMccMncForDownload))) { mCarrierId = carrierId; updateSimOperator(); // If device is screen locked do not proceed to handle // EVENT_ALARM_OR_CONFIG_CHANGE if (mKeyguardManager.isDeviceLocked()) { logd("Device is Locked"); mIsRequiredToHandleUnlock = true; } else { logd("Carrier Config changed: slotIndex=" + slotIndex); sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE); } } } else { boolean isUserUnlocked = mUserManager.isUserUnlocked(); if (isUserUnlocked && slotIndex == mPhone.getPhoneId()) { Log.d(LOG_TAG, "Carrier Config changed: slotIndex=" + slotIndex); handleAlarmOrConfigChange(); } else { Log.d(LOG_TAG, "User is locked"); mContext.registerReceiver(mUserUnlockedReceiver, new IntentFilter( Intent.ACTION_USER_UNLOCKED)); } } }); mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); } // TODO remove this method upon imsiKeyRetryDownloadOnPhoneUnlock enabled. private final BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { Log.d(LOG_TAG, "Received UserUnlockedReceiver"); handleAlarmOrConfigChange(); } } }; private final BroadcastReceiver mDownloadReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) { logd("Download Complete"); sendMessage(obtainMessage(EVENT_DOWNLOAD_COMPLETE, intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0))); } } }; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId()); int phoneId = mPhone.getPhoneId(); switch (action) { case INTENT_KEY_RENEWAL_ALARM_PREFIX -> { int slotIndexExtra = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX, -1); if (slotIndexExtra == slotIndex) { logd("Handling key renewal alarm: " + action); if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { updateSimOperator(); } sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE); } } case TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD -> { if (phoneId == intent.getIntExtra(PhoneConstants.PHONE_KEY, SubscriptionManager.INVALID_SIM_SLOT_INDEX)) { logd("Handling reset intent: " + action); sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE); } } case Intent.ACTION_USER_UNLOCKED -> { // The Carrier key download fails when SIM is inserted while device is locked // hence adding a retry logic when device is unlocked. logd("device fully unlocked, isRequiredToHandleUnlock = " + mIsRequiredToHandleUnlock + ", slotIndex = " + slotIndex + " hasActiveDataNetwork = " + ( mConnectivityManager.getActiveNetwork() != null)); if (mIsRequiredToHandleUnlock) { mIsRequiredToHandleUnlock = false; sendEmptyMessage(EVENT_SCREEN_UNLOCKED); } } } } }; @Override public void handleMessage (Message msg) { switch (msg.what) { case EVENT_ALARM_OR_CONFIG_CHANGE, EVENT_NETWORK_AVAILABLE, EVENT_SCREEN_UNLOCKED -> handleAlarmOrConfigChange(); case EVENT_DOWNLOAD_COMPLETE -> { long carrierKeyDownloadIdentifier = (long) msg.obj; String currentMccMnc = Flags.imsiKeyRetryDownloadOnPhoneUnlock() ? mTelephonyManager.getSimOperator(mPhone.getSubId()) : getSimOperator(); int carrierId = Flags.imsiKeyRetryDownloadOnPhoneUnlock() ? mTelephonyManager.getSimCarrierId() : getSimCarrierId(); if (isValidDownload(currentMccMnc, carrierKeyDownloadIdentifier, carrierId)) { onDownloadComplete(carrierKeyDownloadIdentifier, currentMccMnc, carrierId); onPostDownloadProcessing(); } } } } private void onPostDownloadProcessing() { resetRenewalAlarm(); if(Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { mDownloadId = -1; } else { cleanupDownloadInfo(); } // unregister from DOWNLOAD_COMPLETE mContext.unregisterReceiver(mDownloadReceiver); } private void handleAlarmOrConfigChange() { if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { if (carrierUsesKeys()) { if (areCarrierKeysAbsentOrExpiring()) { boolean hasActiveDataNetwork = (mConnectivityManager.getActiveNetwork() != null); boolean downloadStartedSuccessfully = hasActiveDataNetwork && downloadKey(); // if the download was attempted, but not started successfully, and if // carriers uses keys, we'll still want to renew the alarms, and try // downloading the key a day later. int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId()); if (downloadStartedSuccessfully) { unregisterDefaultNetworkCb(slotIndex); } else { // If download fails due to the device lock, we will reattempt once the // device is unlocked. mIsRequiredToHandleUnlock = mKeyguardManager.isDeviceLocked(); loge("hasActiveDataConnection = " + hasActiveDataNetwork + " isDeviceLocked = " + mIsRequiredToHandleUnlock); if (!hasActiveDataNetwork) { registerDefaultNetworkCb(slotIndex); } resetRenewalAlarm(); } } logd("handleAlarmOrConfigChange :: areCarrierKeysAbsentOrExpiring returned false"); } else { cleanupRenewalAlarms(); if (!isOtherSlotHasCarrier()) { // delete any existing alarms. mPhone.deleteCarrierInfoForImsiEncryption(getSimCarrierId(), getSimOperator()); } cleanupDownloadInfo(); } } else { if (carrierUsesKeys()) { if (areCarrierKeysAbsentOrExpiring()) { boolean downloadStartedSuccessfully = downloadKey(); // if the download was attempted, but not started successfully, and if // carriers uses keys, we'll still want to renew the alarms, and try // downloading the key a day later. if (!downloadStartedSuccessfully) { resetRenewalAlarm(); } } } else { // delete any existing alarms. cleanupRenewalAlarms(); mPhone.deleteCarrierInfoForImsiEncryption(getSimCarrierId()); } } } private boolean isOtherSlotHasCarrier() { SubscriptionManager subscriptionManager = mPhone.getContext().getSystemService( SubscriptionManager.class); List subscriptionInfoList = subscriptionManager.getActiveSubscriptionInfoList(); loge("handleAlarmOrConfigChange ActiveSubscriptionInfoList = " + ( (subscriptionInfoList != null) ? subscriptionInfoList.size() : null)); for (SubscriptionInfo subInfo : subscriptionInfoList) { if (mPhone.getSubId() != subInfo.getSubscriptionId() && subInfo.getCarrierId() == mPhone.getCarrierId()) { // We do not proceed to remove the Key from the DB as another slot contains // same operator sim which is in active state. loge("handleAlarmOrConfigChange same operator sim in another slot"); return true; } } return false; } private void cleanupDownloadInfo() { logd("Cleaning up download info"); mDownloadId = -1; if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { mMccMncForDownload = ""; } else { mMccMncForDownload = null; } mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; } private void cleanupRenewalAlarms() { logd("Cleaning up existing renewal alarms"); int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId()); Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX); intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex); PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); AlarmManager alarmManager =mContext.getSystemService(AlarmManager.class); alarmManager.cancel(carrierKeyDownloadIntent); } /** * this method returns the date to be used to decide on when to start downloading the key. * from the carrier. **/ @VisibleForTesting public long getExpirationDate() { long minExpirationDate = Long.MAX_VALUE; for (int key_type : CARRIER_KEY_TYPES) { if (!isKeyEnabled(key_type)) { continue; } ImsiEncryptionInfo imsiEncryptionInfo = mPhone.getCarrierInfoForImsiEncryption(key_type, false); if (imsiEncryptionInfo != null && imsiEncryptionInfo.getExpirationTime() != null) { if (minExpirationDate > imsiEncryptionInfo.getExpirationTime().getTime()) { minExpirationDate = imsiEncryptionInfo.getExpirationTime().getTime(); } } } // if there are no keys, or expiration date is in the past, or within 7 days, then we // set the alarm to run in a day. Else, we'll set the alarm to run 7 days prior to // expiration. if (minExpirationDate == Long.MAX_VALUE || (minExpirationDate < System.currentTimeMillis() + END_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) { minExpirationDate = System.currentTimeMillis() + DAY_IN_MILLIS; } else { // We don't want all the phones to download the certs simultaneously, so // we pick a random time during the download window to avoid this situation. Random random = new Random(); int max = START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS; int min = END_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS; int randomTime = random.nextInt(max - min) + min; minExpirationDate = minExpirationDate - randomTime; } return minExpirationDate; } /** * this method resets the alarm. Starts by cleaning up the existing alarms. * We look at the earliest expiration date, and setup an alarms X days prior. * If the expiration date is in the past, we'll setup an alarm to run the next day. This * could happen if the download has failed. **/ @VisibleForTesting public void resetRenewalAlarm() { cleanupRenewalAlarms(); int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId()); long minExpirationDate = getExpirationDate(); logd("minExpirationDate: " + new Date(minExpirationDate)); final AlarmManager alarmManager = (AlarmManager) mContext.getSystemService( Context.ALARM_SERVICE); Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX); intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex); PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); alarmManager.set(AlarmManager.RTC_WAKEUP, minExpirationDate, carrierKeyDownloadIntent); logd("setRenewalAlarm: action=" + intent.getAction() + " time=" + new Date(minExpirationDate)); } /** * Read the store the sim operetor value and update the value in case of change in the sim * operetor. */ public void updateSimOperator() { if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { String simOperator = mPhone.getOperatorNumeric(); if (!TextUtils.isEmpty(simOperator) && !simOperator.equals(mMccMncForDownload)) { mMccMncForDownload = simOperator; logd("updateSimOperator, Initialized mMccMncForDownload = " + mMccMncForDownload); } } } /** * Returns the sim operator. **/ @VisibleForTesting public String getSimOperator() { if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { updateSimOperator(); return mMccMncForDownload; } else { return mTelephonyManager.getSimOperator(mPhone.getSubId()); } } /** * Returns the sim operator. **/ @VisibleForTesting public int getSimCarrierId() { if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { return (mCarrierId > 0) ? mCarrierId : mPhone.getCarrierId(); } else { return mTelephonyManager.getSimCarrierId(); } } /** * checks if the download was sent by this particular instance. We do this by including the * slot id in the key. If no value is found, we know that the download was not for this * instance of the phone. **/ @VisibleForTesting public boolean isValidDownload(String currentMccMnc, long currentDownloadId, int carrierId) { if (currentDownloadId != mDownloadId) { loge( "download ID=" + currentDownloadId + " for completed download does not match stored id=" + mDownloadId); return false; } if (TextUtils.isEmpty(currentMccMnc) || TextUtils.isEmpty(mMccMncForDownload) || !TextUtils.equals(currentMccMnc, mMccMncForDownload) || mCarrierId != carrierId) { loge( "currentMccMnc=" + currentMccMnc + " storedMccMnc =" + mMccMncForDownload + "currentCarrierId = " + carrierId + " storedCarrierId = " + mCarrierId); return false; } logd("Matched MccMnc = " + currentMccMnc + ", carrierId = " + carrierId + ", downloadId: " + currentDownloadId); return true; } /** * This method will try to parse the downloaded information, and persist it in the database. **/ private void onDownloadComplete(long carrierKeyDownloadIdentifier, String mccMnc, int carrierId) { logd("onDownloadComplete: " + carrierKeyDownloadIdentifier); String jsonStr; DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(carrierKeyDownloadIdentifier); Cursor cursor = mDownloadManager.query(query); if (cursor == null) { return; } if (cursor.moveToFirst()) { int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) { try { jsonStr = convertToString(mDownloadManager, carrierKeyDownloadIdentifier); if (TextUtils.isEmpty(jsonStr)) { logd("fallback to no gzip"); jsonStr = convertToStringNoGZip(mDownloadManager, carrierKeyDownloadIdentifier); } parseJsonAndPersistKey(jsonStr, mccMnc, carrierId); } catch (Exception e) { loge( "Error in download:" + carrierKeyDownloadIdentifier + ". " + e); } finally { mDownloadManager.remove(carrierKeyDownloadIdentifier); } } logd("Completed downloading keys"); } cursor.close(); } /** * This method checks if the carrier requires key. We'll read the carrier config to make that * determination. * @return boolean returns true if carrier requires keys, else false. **/ private boolean carrierUsesKeys() { CarrierConfigManager carrierConfigManager = (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); if (carrierConfigManager == null) { return false; } int subId = mPhone.getSubId(); PersistableBundle b = null; try { b = carrierConfigManager.getConfigForSubId(subId, CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT, CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING, CarrierConfigManager.KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL); } catch (RuntimeException e) { loge( "CarrierConfigLoader is not available."); } if (b == null || b.isEmpty()) { return false; } mKeyAvailability = b.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT); mURL = b.getString(CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING); mAllowedOverMeteredNetwork = b.getBoolean( CarrierConfigManager.KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL); if (mKeyAvailability == 0 || TextUtils.isEmpty(mURL)) { logd("Carrier not enabled or invalid values. mKeyAvailability=" + mKeyAvailability + " mURL=" + mURL); return false; } for (int key_type : CARRIER_KEY_TYPES) { if (isKeyEnabled(key_type)) { return true; } } return false; } private static String convertToStringNoGZip(DownloadManager downloadManager, long downloadId) { StringBuilder sb = new StringBuilder(); try (InputStream source = new FileInputStream( downloadManager.openDownloadedFile(downloadId).getFileDescriptor())) { // If the carrier does not have the data gzipped, fallback to assuming it is not zipped. // parseJsonAndPersistKey may still fail if the data is malformed, so we won't be // persisting random bogus strings thinking it's the cert BufferedReader reader = new BufferedReader(new InputStreamReader(source, UTF_8)); String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); } } catch (IOException e) { e.printStackTrace(); return null; } return sb.toString(); } private static String convertToString(DownloadManager downloadManager, long downloadId) { try (InputStream source = new FileInputStream( downloadManager.openDownloadedFile(downloadId).getFileDescriptor()); InputStream gzipIs = new GZIPInputStream(source)) { BufferedReader reader = new BufferedReader(new InputStreamReader(gzipIs, UTF_8)); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); } return sb.toString(); } catch (ZipException e) { // GZIPInputStream constructor will throw exception if stream is not GZIP Log.d(LOG_TAG, "Stream is not gzipped e=" + e); return null; } catch (IOException e) { Log.e(LOG_TAG, "Unexpected exception in convertToString e=" + e); return null; } } /** * Converts the string into a json object to retreive the nodes. The Json should have 3 nodes, * including the Carrier public key, the key type and the key identifier. Once the nodes have * been extracted, they get persisted to the database. Sample: * "carrier-keys": [ { "certificate": "", * "key-type": "WLAN", * "key-identifier": "" * } ] * @param jsonStr the json string. * @param mccMnc contains the mcc, mnc. */ @VisibleForTesting public void parseJsonAndPersistKey(String jsonStr, String mccMnc, int carrierId) { if (TextUtils.isEmpty(jsonStr) || TextUtils.isEmpty(mccMnc) || carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { loge( "jsonStr or mcc, mnc: is empty or carrierId is UNKNOWN_CARRIER_ID"); return; } try { String mcc = mccMnc.substring(0, 3); String mnc = mccMnc.substring(3); JSONObject jsonObj = new JSONObject(jsonStr); JSONArray keys = jsonObj.getJSONArray(JSON_CARRIER_KEYS); for (int i = 0; i < keys.length(); i++) { JSONObject key = keys.getJSONObject(i); // Support both "public-key" and "certificate" String property. String cert = null; if (key.has(JSON_CERTIFICATE)) { cert = key.getString(JSON_CERTIFICATE); } else { cert = key.getString(JSON_CERTIFICATE_ALTERNATE); } // The key-type property is optional, therefore, the default value is WLAN type if // not specified. int type = TelephonyManager.KEY_TYPE_WLAN; if (key.has(JSON_TYPE)) { String typeString = key.getString(JSON_TYPE); if (typeString.equals(JSON_TYPE_VALUE_EPDG)) { type = TelephonyManager.KEY_TYPE_EPDG; } else if (!typeString.equals(JSON_TYPE_VALUE_WLAN)) { loge( "Invalid key-type specified: " + typeString); } } String identifier = key.getString(JSON_IDENTIFIER); Pair keyInfo = getKeyInformation(cleanCertString(cert).getBytes()); if (mDeleteOldKeyAfterDownload) { if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { mPhone.deleteCarrierInfoForImsiEncryption( TelephonyManager.UNKNOWN_CARRIER_ID, null); } else { mPhone.deleteCarrierInfoForImsiEncryption( TelephonyManager.UNKNOWN_CARRIER_ID); } mDeleteOldKeyAfterDownload = false; } savePublicKey(keyInfo.first, type, identifier, keyInfo.second, mcc, mnc, carrierId); } } catch (final JSONException e) { loge( "Json parsing error: " + e.getMessage()); } catch (final Exception e) { loge( "Exception getting certificate: " + e); } } /** * introspects the mKeyAvailability bitmask * @return true if the digit at position k is 1, else false. */ @VisibleForTesting public boolean isKeyEnabled(int keyType) { // since keytype has values of 1, 2.... we need to subtract 1 from the keytype. return isKeyEnabled(keyType, mKeyAvailability); } /** * introspects the mKeyAvailability bitmask * @return true if the digit at position k is 1, else false. */ public static boolean isKeyEnabled(int keyType, int keyAvailability) { // since keytype has values of 1, 2.... we need to subtract 1 from the keytype. int returnValue = (keyAvailability >> (keyType - 1)) & 1; return returnValue == 1; } /** * Checks whether is the keys are absent or close to expiration. Returns true, if either of * those conditions are true. * @return boolean returns true when keys are absent or close to expiration, else false. */ @VisibleForTesting public boolean areCarrierKeysAbsentOrExpiring() { for (int key_type : CARRIER_KEY_TYPES) { if (!isKeyEnabled(key_type)) { continue; } // get encryption info with fallback=false so that we attempt a download even if there's // backup info stored in carrier config ImsiEncryptionInfo imsiEncryptionInfo = mPhone.getCarrierInfoForImsiEncryption(key_type, false); if (imsiEncryptionInfo == null) { logd("Key not found for: " + key_type); return true; } else if (imsiEncryptionInfo.getCarrierId() == TelephonyManager.UNKNOWN_CARRIER_ID) { logd("carrier key is unknown carrier, so prefer to reDownload"); mDeleteOldKeyAfterDownload = true; return true; } Date imsiDate = imsiEncryptionInfo.getExpirationTime(); long timeToExpire = imsiDate.getTime() - System.currentTimeMillis(); return timeToExpire < START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS; } return false; } private boolean downloadKey() { logd("starting download from: " + mURL); String mccMnc = null; int carrierId = -1; if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { if (TextUtils.isEmpty(mMccMncForDownload) || mCarrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { loge("mccmnc or carrierId is UnKnown"); return false; } } else { mccMnc = getSimOperator(); carrierId = getSimCarrierId(); if (!TextUtils.isEmpty(mccMnc) || carrierId != TelephonyManager.UNKNOWN_CARRIER_ID) { Log.d(LOG_TAG, "downloading key for mccmnc : " + mccMnc + ", carrierId : " + carrierId); } else { Log.e(LOG_TAG, "mccmnc or carrierId is UnKnown"); return false; } } try { // register the broadcast receiver to listen for download complete IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); mContext.registerReceiver(mDownloadReceiver, filter, null, mPhone, Context.RECEIVER_EXPORTED); DownloadManager.Request request = new DownloadManager.Request(Uri.parse(mURL)); // TODO(b/128550341): Implement the logic to minimize using metered network such as // LTE for downloading a certificate. request.setAllowedOverMetered(mAllowedOverMeteredNetwork); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); request.addRequestHeader("Accept-Encoding", "gzip"); long carrierKeyDownloadRequestId = mDownloadManager.enqueue(request); if (!Flags.imsiKeyRetryDownloadOnPhoneUnlock()) { mMccMncForDownload = mccMnc; mCarrierId = carrierId; } mDownloadId = carrierKeyDownloadRequestId; logd("saving values mccmnc: " + mMccMncForDownload + ", downloadId: " + carrierKeyDownloadRequestId + ", carrierId: " + mCarrierId); } catch (Exception e) { loge( "exception trying to download key from url: " + mURL + ", Exception = " + e.getMessage()); return false; } return true; } /** * Save the public key * @param certificate certificate that contains the public key. * @return Pair containing the Public Key and the expiration date. **/ @VisibleForTesting public static Pair getKeyInformation(byte[] certificate) throws Exception { InputStream inStream = new ByteArrayInputStream(certificate); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream); Pair keyInformation = new Pair<>(cert.getPublicKey(), cert.getNotAfter().getTime()); return keyInformation; } /** * Save the public key * @param publicKey public key. * @param type key-type. * @param identifier which is an opaque string. * @param expirationDate expiration date of the key. * @param mcc * @param mnc **/ @VisibleForTesting public void savePublicKey(PublicKey publicKey, int type, String identifier, long expirationDate, String mcc, String mnc, int carrierId) { ImsiEncryptionInfo imsiEncryptionInfo = new ImsiEncryptionInfo(mcc, mnc, type, identifier, publicKey, new Date(expirationDate), carrierId); mPhone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo); } /** * Remove potential extraneous text in a certificate string * @param cert certificate string * @return Cleaned up version of the certificate string */ @VisibleForTesting public static String cleanCertString(String cert) { return cert.substring( cert.indexOf(CERT_BEGIN_STRING), cert.indexOf(CERT_END_STRING) + CERT_END_STRING.length()); } /** * Registering the callback to listen on data connection availability. * * @param slotId Sim slotIndex that tries to download the key. */ private void registerDefaultNetworkCb(int slotId) { logd("RegisterDefaultNetworkCb for slotId = " + slotId); if (mDefaultNetworkCallback == null && slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) { mDefaultNetworkCallback = new DefaultNetworkCallback(slotId); mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, this); } } /** * Unregister the data connection monitor listener. */ private void unregisterDefaultNetworkCb(int slotId) { logd("unregisterDefaultNetworkCb for slotId = " + slotId); if (mDefaultNetworkCallback != null) { mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback); mDefaultNetworkCallback = null; } } final class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback { final int mSlotIndex; public DefaultNetworkCallback(int slotId) { mSlotIndex = slotId; } /** Called when the framework connects and has declared a new network ready for use. */ @Override public void onAvailable(@NonNull Network network) { logd("Data network connected, slotId = " + mSlotIndex); if (mConnectivityManager.getActiveNetwork() != null && SubscriptionManager.getSlotIndex( mPhone.getSubId()) == mSlotIndex) { mIsRequiredToHandleUnlock = false; unregisterDefaultNetworkCb(mSlotIndex); sendEmptyMessage(EVENT_NETWORK_AVAILABLE); } } } private void loge(String logStr) { String TAG = LOG_TAG + " [" + mPhone.getPhoneId() + "]"; Log.e(TAG, logStr); } private void logd(String logStr) { String TAG = LOG_TAG + " [" + mPhone.getPhoneId() + "]"; Log.d(TAG, logStr); } }