blob: 71e9dc5ef6d7ec1e0e94c7b4500d98319a34ebf6 [file] [log] [blame]
/*
* 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 android.preference.PreferenceManager.getDefaultSharedPreferences;
import static android.telephony.CarrierConfigManager.KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.app.AlarmManager;
import android.app.DownloadManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.telephony.ImsiEncryptionInfo;
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.org.bouncycastle.util.io.pem.PemReader;
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.io.Reader;
import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Random;
import java.util.zip.GZIPInputStream;
/**
* 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 MCC_MNC_PREF_TAG = "CARRIER_KEY_DM_MCC_MNC";
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;
public static final String MNC = "MNC";
public static final String MCC = "MCC";
private static final String SEPARATOR = ":";
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[] 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;
public CarrierKeyDownloadManager(Phone phone) {
mPhone = phone;
mContext = phone.getContext();
IntentFilter filter = new IntentFilter();
filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
filter.addAction(INTENT_KEY_RENEWAL_ALARM_PREFIX + mPhone.getPhoneId());
filter.addAction(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD);
mContext.registerReceiver(mBroadcastReceiver, filter, null, phone);
mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
int slotId = mPhone.getPhoneId();
if (action.equals(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId)) {
Log.d(LOG_TAG, "Handling key renewal alarm: " + action);
sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE);
} else if (action.equals(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD)) {
if (slotId == intent.getIntExtra(PhoneConstants.PHONE_KEY,
SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
Log.d(LOG_TAG, "Handling reset intent: " + action);
sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE);
}
} else if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
if (slotId == intent.getIntExtra(PhoneConstants.PHONE_KEY,
SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
Log.d(LOG_TAG, "Carrier Config changed: " + action);
sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE);
}
} else if (action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
Log.d(LOG_TAG, "Download Complete");
sendMessage(obtainMessage(EVENT_DOWNLOAD_COMPLETE,
intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)));
}
}
};
@Override
public void handleMessage (Message msg) {
switch (msg.what) {
case EVENT_ALARM_OR_CONFIG_CHANGE:
handleAlarmOrConfigChange();
break;
case EVENT_DOWNLOAD_COMPLETE:
long carrierKeyDownloadIdentifier = (long) msg.obj;
String mccMnc = getMccMncSetFromPref();
if (isValidDownload(mccMnc)) {
onDownloadComplete(carrierKeyDownloadIdentifier, mccMnc);
onPostDownloadProcessing(carrierKeyDownloadIdentifier);
}
break;
}
}
private void onPostDownloadProcessing(long carrierKeyDownloadIdentifier) {
resetRenewalAlarm();
cleanupDownloadPreferences(carrierKeyDownloadIdentifier);
}
private void handleAlarmOrConfigChange() {
if (carrierUsesKeys()) {
if (areCarrierKeysAbsentOrExpiring()) {
boolean downloadStartedSuccessfully = downloadKey();
// if the download was attemped, 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 {
return;
}
} else {
// delete any existing alarms.
cleanupRenewalAlarms();
}
}
private void cleanupDownloadPreferences(long carrierKeyDownloadIdentifier) {
Log.d(LOG_TAG, "Cleaning up download preferences: " + carrierKeyDownloadIdentifier);
SharedPreferences.Editor editor = getDefaultSharedPreferences(mContext).edit();
editor.remove(String.valueOf(carrierKeyDownloadIdentifier));
editor.commit();
}
private void cleanupRenewalAlarms() {
Log.d(LOG_TAG, "Cleaning up existing renewal alarms");
int slotId = mPhone.getPhoneId();
Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId);
PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager =
(AlarmManager) mContext.getSystemService(mContext.ALARM_SERVICE);
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);
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 slotId = mPhone.getPhoneId();
long minExpirationDate = getExpirationDate();
Log.d(LOG_TAG, "minExpirationDate: " + new Date(minExpirationDate));
final AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId);
PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, minExpirationDate,
carrierKeyDownloadIntent);
Log.d(LOG_TAG, "setRenewelAlarm: action=" + intent.getAction() + " time="
+ new Date(minExpirationDate));
}
private String getMccMncSetFromPref() {
// check if this is a download that we had created. We do this by checking if the
// downloadId is stored in the shared prefs.
int slotId = mPhone.getPhoneId();
SharedPreferences preferences = getDefaultSharedPreferences(mContext);
return preferences.getString(MCC_MNC_PREF_TAG + slotId, null);
}
/**
* Returns the sim operator.
**/
@VisibleForTesting
public String getSimOperator() {
final TelephonyManager telephonyManager =
(TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
return telephonyManager.getSimOperator(mPhone.getSubId());
}
/**
* 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 mccMnc) {
String mccCurrent = "";
String mncCurrent = "";
String mccSource = "";
String mncSource = "";
String simOperator = getSimOperator();
if (TextUtils.isEmpty(simOperator) || TextUtils.isEmpty(mccMnc)) {
Log.e(LOG_TAG, "simOperator or mcc/mnc is empty");
return false;
}
String[] splitValue = mccMnc.split(SEPARATOR);
mccSource = splitValue[0];
mncSource = splitValue[1];
Log.d(LOG_TAG, "values from sharedPrefs mcc, mnc: " + mccSource + "," + mncSource);
mccCurrent = simOperator.substring(0, 3);
mncCurrent = simOperator.substring(3);
Log.d(LOG_TAG, "using values for mcc, mnc: " + mccCurrent + "," + mncCurrent);
if (TextUtils.equals(mncSource, mncCurrent) && TextUtils.equals(mccSource, mccCurrent)) {
return true;
}
return false;
}
/**
* This method will try to parse the downloaded information, and persist it in the database.
**/
private void onDownloadComplete(long carrierKeyDownloadIdentifier, String mccMnc) {
Log.d(LOG_TAG, "onDownloadComplete: " + carrierKeyDownloadIdentifier);
String jsonStr;
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(carrierKeyDownloadIdentifier);
Cursor cursor = mDownloadManager.query(query);
InputStream source = null;
if (cursor == null) {
return;
}
if (cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) {
try {
source = new FileInputStream(
mDownloadManager.openDownloadedFile(carrierKeyDownloadIdentifier)
.getFileDescriptor());
jsonStr = convertToString(source);
parseJsonAndPersistKey(jsonStr, mccMnc);
} catch (Exception e) {
Log.e(LOG_TAG, "Error in download:" + carrierKeyDownloadIdentifier
+ ". " + e);
} finally {
mDownloadManager.remove(carrierKeyDownloadIdentifier);
try {
source.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Log.d(LOG_TAG, "Completed downloading keys");
}
cursor.close();
return;
}
/**
* 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 = carrierConfigManager.getConfigForSubId(subId);
if (b == null) {
return false;
}
mKeyAvailability = b.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT);
mURL = b.getString(CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING);
mAllowedOverMeteredNetwork = b.getBoolean(
KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL);
if (TextUtils.isEmpty(mURL) || mKeyAvailability == 0) {
Log.d(LOG_TAG, "Carrier not enabled or invalid values");
return false;
}
for (int key_type : CARRIER_KEY_TYPES) {
if (isKeyEnabled(key_type)) {
return true;
}
}
return false;
}
private static String convertToString(InputStream is) {
try {
// The current implementation at certain Carriers has the data gzipped, which requires
// us to unzip the contents. Longer term, we want to add a flag in carrier config which
// determines if the data needs to be zipped or not.
GZIPInputStream gunzip = new GZIPInputStream(is);
BufferedReader reader = new BufferedReader(new InputStreamReader(gunzip, UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
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) {
if (TextUtils.isEmpty(jsonStr) || TextUtils.isEmpty(mccMnc)) {
Log.e(LOG_TAG, "jsonStr or mcc, mnc: is empty");
return;
}
PemReader reader = null;
try {
String mcc = "";
String mnc = "";
String[] splitValue = mccMnc.split(SEPARATOR);
mcc = splitValue[0];
mnc = splitValue[1];
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.
// "certificate" is a more accurate description, however, the 3GPP draft spec
// S3-170116, "Privacy Protection for EAP-AKA" section 4.3 mandates the use of
// "public-key".
String cert = null;
if (key.has(JSON_CERTIFICATE)) {
cert = key.getString(JSON_CERTIFICATE);
} else {
cert = key.getString(JSON_CERTIFICATE_ALTERNATE);
}
// The 3GPP draft spec 3GPP draft spec S3-170116, "Privacy Protection for EAP-AKA"
// section 4.3, does not specify any key-type property. To be compatible with these
// networks, the logic defaults to 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)) {
Log.e(LOG_TAG, "Invalid key-type specified: " + typeString);
}
}
String identifier = key.getString(JSON_IDENTIFIER);
ByteArrayInputStream inStream = new ByteArrayInputStream(cert.getBytes());
Reader fReader = new BufferedReader(new InputStreamReader(inStream));
reader = new PemReader(fReader);
Pair<PublicKey, Long> keyInfo =
getKeyInformation(reader.readPemObject().getContent());
reader.close();
savePublicKey(keyInfo.first, type, identifier, keyInfo.second, mcc, mnc);
}
} catch (final JSONException e) {
Log.e(LOG_TAG, "Json parsing error: " + e.getMessage());
} catch (final Exception e) {
Log.e(LOG_TAG, "Exception getting certificate: " + e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (final Exception e) {
Log.e(LOG_TAG, "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.
int returnValue = (mKeyAvailability >> (keyType - 1)) & 1;
return (returnValue == 1) ? true : false;
}
/**
* 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;
}
ImsiEncryptionInfo imsiEncryptionInfo =
mPhone.getCarrierInfoForImsiEncryption(key_type);
if (imsiEncryptionInfo == null) {
Log.d(LOG_TAG, "Key not found for: " + key_type);
return true;
}
Date imsiDate = imsiEncryptionInfo.getExpirationTime();
long timeToExpire = imsiDate.getTime() - System.currentTimeMillis();
return (timeToExpire < START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS) ? true : false;
}
return false;
}
private boolean downloadKey() {
Log.d(LOG_TAG, "starting download from: " + mURL);
String mcc = "";
String mnc = "";
String simOperator = getSimOperator();
if (!TextUtils.isEmpty(simOperator)) {
mcc = simOperator.substring(0, 3);
mnc = simOperator.substring(3);
Log.d(LOG_TAG, "using values for mcc, mnc: " + mcc + "," + mnc);
} else {
Log.e(LOG_TAG, "mcc, mnc: is empty");
return false;
}
try {
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.setVisibleInDownloadsUi(false);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
Long carrierKeyDownloadRequestId = mDownloadManager.enqueue(request);
SharedPreferences.Editor editor = getDefaultSharedPreferences(mContext).edit();
String mccMnc = mcc + SEPARATOR + mnc;
int slotId = mPhone.getPhoneId();
Log.d(LOG_TAG, "storing values in sharedpref mcc, mnc, days: " + mcc + "," + mnc
+ "," + carrierKeyDownloadRequestId);
editor.putString(MCC_MNC_PREF_TAG + slotId, mccMnc);
editor.commit();
} catch (Exception e) {
Log.e(LOG_TAG, "exception trying to dowload key from url: " + mURL);
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) {
ImsiEncryptionInfo imsiEncryptionInfo = new ImsiEncryptionInfo(mcc, mnc, type, identifier,
publicKey, new Date(expirationDate));
mPhone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo);
}
}