Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cf034892 authored by pkanwar's avatar pkanwar Committed by Pankaj Kanwar
Browse files

Building support for IMSI encryption.

Adding a Download Manager to download the keys.
BUG: 35606429
Test: manual. adding tests in a different CL.

Change-Id: I0683a8113c3a3b156ea915a457eeb5f500583424
(cherry picked from commit bef4b766)
parent f2518bd2
Loading
Loading
Loading
Loading
+471 −0
Original line number Diff line number Diff line
/*
 * 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 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.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.telephony.ImsiEncryptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Date;

/**
 * This class contains logic to get Certificates and keep them current.
 * The class will be instantiated by various Phone implementations.
 */
public class CarrierKeyDownloadManager {
    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;

    // Start trying to renew the cert X days before it expires.
    private static final int DEFAULT_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";

    private 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_KEY = "key";
    private static final String JSON_TYPE = "type";
    private static final String JSON_IDENTIFIER = "identifier";
    private static final String JSON_EXPIRATION_DATE = "expiration-date";
    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[] CARRIER_KEY_TYPES = {TelephonyManager.KEY_TYPE_EPDG,
            TelephonyManager.KEY_TYPE_WLAN};
    private static final int UNINITIALIZED_KEY_TYPE = -1;

    private final Phone mPhone;
    private final Context mContext;
    private final DownloadManager mDownloadManager;
    private String mURL;

    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());
        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);
                handleAlarmOrConfigChange();
            } 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);
                    handleAlarmOrConfigChange();
                }
            } else if (action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
                Log.d(LOG_TAG, "Download Complete");
                long carrierKeyDownloadIdentifier =
                        intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
                String mccMnc = getMccMncSetFromPref();
                if (isValidDownload(mccMnc)) {
                    onDownloadComplete(carrierKeyDownloadIdentifier, mccMnc);
                    onPostDownloadProcessing(carrierKeyDownloadIdentifier);
                }
            }
        }
    };

    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);
        AlarmManager alarmManager =
                (AlarmManager) mContext.getSystemService(mContext.ALARM_SERVICE);
        alarmManager.cancel(carrierKeyDownloadIntent);
    }

    /**
     * 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.
     **/
    private void resetRenewalAlarm() {
        cleanupRenewalAlarms();
        int slotId = mPhone.getPhoneId();
        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() + DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) {
            minExpirationDate = System.currentTimeMillis() + DAY_IN_MILLIS;
        } else {
            minExpirationDate = minExpirationDate - DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
        }
        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);
        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);
    }

    /**
     *  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.
     **/
    private boolean isValidDownload(String mccMnc) {
        String mccCurrent = "";
        String mncCurrent = "";
        String mccSource = "";
        String mncSource = "";
        final TelephonyManager telephonyManager =
                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
        String networkOperator = telephonyManager.getNetworkOperator(mPhone.getSubId());

        if (TextUtils.isEmpty(networkOperator) || TextUtils.isEmpty(mccMnc)) {
            Log.e(LOG_TAG, "networkOperator 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 = networkOperator.substring(0, 3);
        mncCurrent = networkOperator.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);

        if (cursor == null) {
            return;
        }
        if (cursor.moveToFirst()) {
            int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
            if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) {
                try {
                    final InputStream 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);
                }
            }
            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);
        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) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();

        String line;
        try {
            while ((line = reader.readLine()) != null) {
                sb.append(line).append('\n');
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }

    /**
     * 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": [ { "key": "",
     *                         "type": WLAN,
     *                         "identifier": "",
     *                         "expiration-date": 1502577746000
     *                        } ]
     * @param jsonStr the json string.
     * @param mccMnc contains the mcc, mnc
     */
    private void parseJsonAndPersistKey(String jsonStr, String mccMnc) {
        if (TextUtils.isEmpty(jsonStr) || TextUtils.isEmpty(mccMnc)) {
            Log.e(LOG_TAG, "jsonStr or mcc, mnc: is empty");
            return;
        }
        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);
                String carrierKey = key.getString(JSON_KEY);
                String typeString = key.getString(JSON_TYPE);
                int type = UNINITIALIZED_KEY_TYPE;
                if (typeString.equals(JSON_TYPE_VALUE_WLAN)) {
                    type = TelephonyManager.KEY_TYPE_WLAN;
                } else if (typeString.equals(JSON_TYPE_VALUE_EPDG)) {
                    type = TelephonyManager.KEY_TYPE_EPDG;
                }
                long expiration_date = key.getLong(JSON_EXPIRATION_DATE);
                String identifier = key.getString(JSON_IDENTIFIER);
                savePublicKey(carrierKey, type, identifier, expiration_date,
                        mcc, mnc);
            }
        } catch (final JSONException e) {
            Log.e(LOG_TAG, "Json parsing error: " + e.getMessage());
        }
    }

    /**
     * introspects the mKeyAvailability bitmask
     * @return true if the digit at position k is 1, else false.
     */

    private 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 < DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS) ? true : false;
        }
        return false;
    }

    private boolean downloadKey() {
        Log.d(LOG_TAG, "starting download from: " + mURL);
        final TelephonyManager telephonyManager =
                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
        String mcc = "";
        String mnc = "";
        String networkOperator = telephonyManager.getNetworkOperator(mPhone.getSubId());

        if (!TextUtils.isEmpty(networkOperator)) {
            mcc = networkOperator.substring(0, 3);
            mnc = networkOperator.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));
            request.setAllowedOverMetered(false);
            request.setVisibleInDownloadsUi(false);
            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;
    }

    private void savePublicKey(String key, int type, String identifier, long expirationDate,
                               String mcc, String mnc) {
        byte[] keyBytes = Base64.decode(key.getBytes(), Base64.DEFAULT);
        ImsiEncryptionInfo imsiEncryptionInfo = new ImsiEncryptionInfo(mcc, mnc, type, identifier,
                keyBytes, new Date(expirationDate));
        mPhone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo);
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -187,7 +187,7 @@ public class GsmCdmaPhone extends Phone {

    private int mRilVersion;
    private boolean mBroadcastEmergencyCallStateChanges = false;

    private CarrierKeyDownloadManager mCDM;
    // Constructors

    public GsmCdmaPhone(Context context, CommandsInterface ci, PhoneNotifier notifier, int phoneId,
@@ -269,6 +269,7 @@ public class GsmCdmaPhone extends Phone {
        mCi.registerForVoiceRadioTechChanged(this, EVENT_VOICE_RADIO_TECH_CHANGED, null);
        mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(
                CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
        mCDM = new CarrierKeyDownloadManager(this);
    }

    private void initRatSpecific(int precisePhoneType) {