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