894 lines
39 KiB
Java
894 lines
39 KiB
Java
/*
|
|
* 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<SubscriptionInfo> 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<PublicKey, Long> 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<PublicKey, Long> getKeyInformation(byte[] certificate) throws Exception {
|
|
InputStream inStream = new ByteArrayInputStream(certificate);
|
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
|
|
Pair<PublicKey, Long> 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);
|
|
}
|
|
}
|