Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2016 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 
     17 package com.android.server.wifi.util;
     18 
     19 import android.net.wifi.WifiConfiguration;
     20 import android.net.wifi.WifiEnterpriseConfig;
     21 import android.telephony.ImsiEncryptionInfo;
     22 import android.telephony.TelephonyManager;
     23 import android.util.Base64;
     24 import android.util.Log;
     25 import android.util.Pair;
     26 
     27 import com.android.internal.annotations.VisibleForTesting;
     28 import com.android.server.wifi.WifiNative;
     29 
     30 import java.security.InvalidKeyException;
     31 import java.security.NoSuchAlgorithmException;
     32 import java.security.PublicKey;
     33 import java.util.HashMap;
     34 
     35 import javax.crypto.BadPaddingException;
     36 import javax.crypto.Cipher;
     37 import javax.crypto.IllegalBlockSizeException;
     38 import javax.crypto.NoSuchPaddingException;
     39 
     40 /**
     41  * Utilities for the Wifi Service to interact with telephony.
     42  */
     43 public class TelephonyUtil {
     44     public static final String TAG = "TelephonyUtil";
     45 
     46     public static final String DEFAULT_EAP_PREFIX = "\0";
     47 
     48     private static final String THREE_GPP_NAI_REALM_FORMAT = "wlan.mnc%s.mcc%s.3gppnetwork.org";
     49 
     50     // IMSI encryption method: RSA-OAEP with SHA-256 hash function
     51     private static final String IMSI_CIPHER_TRANSFORMATION =
     52             "RSA/ECB/OAEPwithSHA-256andMGF1Padding";
     53 
     54     private static final HashMap<Integer, String> EAP_METHOD_PREFIX = new HashMap<>();
     55     static {
     56         EAP_METHOD_PREFIX.put(WifiEnterpriseConfig.Eap.AKA, "0");
     57         EAP_METHOD_PREFIX.put(WifiEnterpriseConfig.Eap.SIM, "1");
     58         EAP_METHOD_PREFIX.put(WifiEnterpriseConfig.Eap.AKA_PRIME, "6");
     59     }
     60 
     61     /**
     62      * Get the identity for the current SIM or null if the SIM is not available
     63      *
     64      * @param tm TelephonyManager instance
     65      * @param config WifiConfiguration that indicates what sort of authentication is necessary
     66      * @return Pair<identify, encrypted identity> or null if the SIM is not available
     67      * or config is invalid
     68      */
     69     public static Pair<String, String> getSimIdentity(TelephonyManager tm,
     70                                                       TelephonyUtil telephonyUtil,
     71                                                       WifiConfiguration config) {
     72         if (tm == null) {
     73             Log.e(TAG, "No valid TelephonyManager");
     74             return null;
     75         }
     76         String imsi = tm.getSubscriberId();
     77         String mccMnc = "";
     78 
     79         if (tm.getSimState() == TelephonyManager.SIM_STATE_READY) {
     80             mccMnc = tm.getSimOperator();
     81         }
     82 
     83         ImsiEncryptionInfo imsiEncryptionInfo;
     84         try {
     85             imsiEncryptionInfo = tm.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN);
     86         } catch (RuntimeException e) {
     87             Log.e(TAG, "Failed to get imsi encryption info: " + e.getMessage());
     88             return null;
     89         }
     90 
     91         String identity = buildIdentity(getSimMethodForConfig(config), imsi, mccMnc, false);
     92         if (identity == null) {
     93             Log.e(TAG, "Failed to build the identity");
     94             return null;
     95         }
     96 
     97         String encryptedIdentity = buildEncryptedIdentity(telephonyUtil,
     98                 getSimMethodForConfig(config), imsi, mccMnc, imsiEncryptionInfo);
     99         // In case of failure for encryption, set empty string
    100         if (encryptedIdentity == null) encryptedIdentity = "";
    101         return Pair.create(identity, encryptedIdentity);
    102     }
    103 
    104     /**
    105      * Encrypt the given data with the given public key.  The encrypted data will be returned as
    106      * a Base64 encoded string.
    107      *
    108      * @param key The public key to use for encryption
    109      * @return Base64 encoded string, or null if encryption failed
    110      */
    111     @VisibleForTesting
    112     public String encryptDataUsingPublicKey(PublicKey key, byte[] data) {
    113         try {
    114             Cipher cipher = Cipher.getInstance(IMSI_CIPHER_TRANSFORMATION);
    115             cipher.init(Cipher.ENCRYPT_MODE, key);
    116             byte[] encryptedBytes = cipher.doFinal(data);
    117             return Base64.encodeToString(encryptedBytes, 0, encryptedBytes.length, Base64.DEFAULT);
    118         } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
    119                 | IllegalBlockSizeException | BadPaddingException e) {
    120             Log.e(TAG, "Encryption failed: " + e.getMessage());
    121             return null;
    122         }
    123     }
    124 
    125     /**
    126      * Create the encrypted IMSI.
    127      *  Prefix value:
    128      * "0" - EAP-AKA Identity
    129      * "1" - EAP-SIM Identity
    130      * "6" - EAP-AKA' Identity
    131      * @param eapMethod EAP authentication method: EAP-SIM, EAP-AKA, EAP-AKA'
    132      * @param imsi The IMSI retrieved from the SIM
    133      * @param mccMnc The MCC MNC identifier retrieved from the SIM
    134      * @param imsiEncryptionInfo The IMSI encryption info retrieved from the SIM
    135      */
    136     private static String buildEncryptedIdentity(TelephonyUtil telephonyUtil, int eapMethod,
    137                                                  String imsi, String mccMnc,
    138                                                  ImsiEncryptionInfo imsiEncryptionInfo) {
    139         if (imsiEncryptionInfo == null) {
    140             return null;
    141         }
    142 
    143         String prefix = EAP_METHOD_PREFIX.get(eapMethod);
    144         if (prefix == null) {
    145             return null;
    146         }
    147         imsi = prefix + imsi;
    148         // Build and return the encrypted identity.
    149         String encryptedImsi = telephonyUtil.encryptDataUsingPublicKey(
    150                 imsiEncryptionInfo.getPublicKey(), imsi.getBytes());
    151         if (encryptedImsi == null) {
    152             Log.e(TAG, "Failed to encrypt IMSI");
    153             return null;
    154         }
    155         String encryptedIdentity = buildIdentity(eapMethod, encryptedImsi, mccMnc, true);
    156         if (imsiEncryptionInfo.getKeyIdentifier() != null) {
    157             // Include key identifier AVP (Attribute Value Pair).
    158             encryptedIdentity = encryptedIdentity + "," + imsiEncryptionInfo.getKeyIdentifier();
    159         }
    160         return encryptedIdentity;
    161     }
    162 
    163     /**
    164      * Create an identity used for SIM-based EAP authentication. The identity will be based on
    165      * the info retrieved from the SIM card, such as IMSI and IMSI encryption info. The IMSI
    166      * contained in the identity will be encrypted if IMSI encryption info is provided.
    167      *
    168      * See  rfc4186 & rfc4187 & rfc5448:
    169      *
    170      * Identity format:
    171      * Prefix | [IMSI || Encrypted IMSI] | @realm | {, Key Identifier AVP}
    172      * where "|" denotes concatenation, "||" denotes exclusive value, "{}"
    173      * denotes optional value, and realm is the 3GPP network domain name derived from the given
    174      * MCC/MNC according to the 3GGP spec(TS23.003).
    175      *
    176      * Prefix value:
    177      * "\0" - Encrypted Identity
    178      * "0" - EAP-AKA Identity
    179      * "1" - EAP-SIM Identity
    180      * "6" - EAP-AKA' Identity
    181      *
    182      * Encrypted IMSI:
    183      * Base64{RSA_Public_Key_Encryption{eapPrefix | IMSI}}
    184      * where "|" denotes concatenation,
    185      *
    186      * @param eapMethod EAP authentication method: EAP-SIM, EAP-AKA, EAP-AKA'
    187      * @param imsi The IMSI retrieved from the SIM
    188      * @param mccMnc The MCC MNC identifier retrieved from the SIM
    189      * @param isEncrypted Whether the imsi is encrypted or not.
    190      * @return the eap identity, built using either the encrypted or un-encrypted IMSI.
    191      */
    192     private static String buildIdentity(int eapMethod, String imsi, String mccMnc,
    193                                         boolean isEncrypted) {
    194         if (imsi == null || imsi.isEmpty()) {
    195             Log.e(TAG, "No IMSI or IMSI is null");
    196             return null;
    197         }
    198 
    199         String prefix = isEncrypted ? DEFAULT_EAP_PREFIX : EAP_METHOD_PREFIX.get(eapMethod);
    200         if (prefix == null) {
    201             return null;
    202         }
    203 
    204         /* extract mcc & mnc from mccMnc */
    205         String mcc;
    206         String mnc;
    207         if (mccMnc != null && !mccMnc.isEmpty()) {
    208             mcc = mccMnc.substring(0, 3);
    209             mnc = mccMnc.substring(3);
    210             if (mnc.length() == 2) {
    211                 mnc = "0" + mnc;
    212             }
    213         } else {
    214             // extract mcc & mnc from IMSI, assume mnc size is 3
    215             mcc = imsi.substring(0, 3);
    216             mnc = imsi.substring(3, 6);
    217         }
    218 
    219         String naiRealm = String.format(THREE_GPP_NAI_REALM_FORMAT, mnc, mcc);
    220         return prefix + imsi + "@" + naiRealm;
    221     }
    222 
    223     /**
    224      * Return the associated SIM method for the configuration.
    225      *
    226      * @param config WifiConfiguration corresponding to the network.
    227      * @return the outer EAP method associated with this SIM configuration.
    228      */
    229     private static int getSimMethodForConfig(WifiConfiguration config) {
    230         if (config == null || config.enterpriseConfig == null) {
    231             return WifiEnterpriseConfig.Eap.NONE;
    232         }
    233         int eapMethod = config.enterpriseConfig.getEapMethod();
    234         if (eapMethod == WifiEnterpriseConfig.Eap.PEAP) {
    235             // Translate known inner eap methods into an equivalent outer eap method.
    236             switch (config.enterpriseConfig.getPhase2Method()) {
    237                 case WifiEnterpriseConfig.Phase2.SIM:
    238                     eapMethod = WifiEnterpriseConfig.Eap.SIM;
    239                     break;
    240                 case WifiEnterpriseConfig.Phase2.AKA:
    241                     eapMethod = WifiEnterpriseConfig.Eap.AKA;
    242                     break;
    243                 case WifiEnterpriseConfig.Phase2.AKA_PRIME:
    244                     eapMethod = WifiEnterpriseConfig.Eap.AKA_PRIME;
    245                     break;
    246             }
    247         }
    248 
    249         return isSimEapMethod(eapMethod) ? eapMethod : WifiEnterpriseConfig.Eap.NONE;
    250     }
    251 
    252     /**
    253      * Checks if the network is a SIM config.
    254      *
    255      * @param config Config corresponding to the network.
    256      * @return true if it is a SIM config, false otherwise.
    257      */
    258     public static boolean isSimConfig(WifiConfiguration config) {
    259         return getSimMethodForConfig(config) != WifiEnterpriseConfig.Eap.NONE;
    260     }
    261 
    262     /**
    263      * Checks if the EAP outer method is SIM related.
    264      *
    265      * @param eapMethod WifiEnterpriseConfig Eap method.
    266      * @return true if this EAP outer method is SIM-related, false otherwise.
    267      */
    268     public static boolean isSimEapMethod(int eapMethod) {
    269         return eapMethod == WifiEnterpriseConfig.Eap.SIM
    270                 || eapMethod == WifiEnterpriseConfig.Eap.AKA
    271                 || eapMethod == WifiEnterpriseConfig.Eap.AKA_PRIME;
    272     }
    273 
    274     // TODO replace some of this code with Byte.parseByte
    275     private static int parseHex(char ch) {
    276         if ('0' <= ch && ch <= '9') {
    277             return ch - '0';
    278         } else if ('a' <= ch && ch <= 'f') {
    279             return ch - 'a' + 10;
    280         } else if ('A' <= ch && ch <= 'F') {
    281             return ch - 'A' + 10;
    282         } else {
    283             throw new NumberFormatException("" + ch + " is not a valid hex digit");
    284         }
    285     }
    286 
    287     private static byte[] parseHex(String hex) {
    288         /* This only works for good input; don't throw bad data at it */
    289         if (hex == null) {
    290             return new byte[0];
    291         }
    292 
    293         if (hex.length() % 2 != 0) {
    294             throw new NumberFormatException(hex + " is not a valid hex string");
    295         }
    296 
    297         byte[] result = new byte[(hex.length()) / 2 + 1];
    298         result[0] = (byte) ((hex.length()) / 2);
    299         for (int i = 0, j = 1; i < hex.length(); i += 2, j++) {
    300             int val = parseHex(hex.charAt(i)) * 16 + parseHex(hex.charAt(i + 1));
    301             byte b = (byte) (val & 0xFF);
    302             result[j] = b;
    303         }
    304 
    305         return result;
    306     }
    307 
    308     private static String makeHex(byte[] bytes) {
    309         StringBuilder sb = new StringBuilder();
    310         for (byte b : bytes) {
    311             sb.append(String.format("%02x", b));
    312         }
    313         return sb.toString();
    314     }
    315 
    316     private static String makeHex(byte[] bytes, int from, int len) {
    317         StringBuilder sb = new StringBuilder();
    318         for (int i = 0; i < len; i++) {
    319             sb.append(String.format("%02x", bytes[from + i]));
    320         }
    321         return sb.toString();
    322     }
    323 
    324     private static byte[] concatHex(byte[] array1, byte[] array2) {
    325 
    326         int len = array1.length + array2.length;
    327 
    328         byte[] result = new byte[len];
    329 
    330         int index = 0;
    331         if (array1.length != 0) {
    332             for (byte b : array1) {
    333                 result[index] = b;
    334                 index++;
    335             }
    336         }
    337 
    338         if (array2.length != 0) {
    339             for (byte b : array2) {
    340                 result[index] = b;
    341                 index++;
    342             }
    343         }
    344 
    345         return result;
    346     }
    347 
    348     public static String getGsmSimAuthResponse(String[] requestData, TelephonyManager tm) {
    349         if (tm == null) {
    350             Log.e(TAG, "No valid TelephonyManager");
    351             return null;
    352         }
    353         StringBuilder sb = new StringBuilder();
    354         for (String challenge : requestData) {
    355             if (challenge == null || challenge.isEmpty()) {
    356                 continue;
    357             }
    358             Log.d(TAG, "RAND = " + challenge);
    359 
    360             byte[] rand = null;
    361             try {
    362                 rand = parseHex(challenge);
    363             } catch (NumberFormatException e) {
    364                 Log.e(TAG, "malformed challenge");
    365                 continue;
    366             }
    367 
    368             String base64Challenge = Base64.encodeToString(rand, Base64.NO_WRAP);
    369 
    370             // Try USIM first for authentication.
    371             String tmResponse = tm.getIccAuthentication(TelephonyManager.APPTYPE_USIM,
    372                     TelephonyManager.AUTHTYPE_EAP_SIM, base64Challenge);
    373             if (tmResponse == null) {
    374                 // Then, in case of failure, issue may be due to sim type, retry as a simple sim
    375                 tmResponse = tm.getIccAuthentication(TelephonyManager.APPTYPE_SIM,
    376                         TelephonyManager.AUTHTYPE_EAP_SIM, base64Challenge);
    377             }
    378             Log.v(TAG, "Raw Response - " + tmResponse);
    379 
    380             if (tmResponse == null || tmResponse.length() <= 4) {
    381                 Log.e(TAG, "bad response - " + tmResponse);
    382                 return null;
    383             }
    384 
    385             byte[] result = Base64.decode(tmResponse, Base64.DEFAULT);
    386             Log.v(TAG, "Hex Response -" + makeHex(result));
    387             int sresLen = result[0];
    388             if (sresLen >= result.length) {
    389                 Log.e(TAG, "malfomed response - " + tmResponse);
    390                 return null;
    391             }
    392             String sres = makeHex(result, 1, sresLen);
    393             int kcOffset = 1 + sresLen;
    394             if (kcOffset >= result.length) {
    395                 Log.e(TAG, "malfomed response - " + tmResponse);
    396                 return null;
    397             }
    398             int kcLen = result[kcOffset];
    399             if (kcOffset + kcLen > result.length) {
    400                 Log.e(TAG, "malfomed response - " + tmResponse);
    401                 return null;
    402             }
    403             String kc = makeHex(result, 1 + kcOffset, kcLen);
    404             sb.append(":" + kc + ":" + sres);
    405             Log.v(TAG, "kc:" + kc + " sres:" + sres);
    406         }
    407 
    408         return sb.toString();
    409     }
    410 
    411     /**
    412      * Data supplied when making a SIM Auth Request
    413      */
    414     public static class SimAuthRequestData {
    415         public SimAuthRequestData() {}
    416         public SimAuthRequestData(int networkId, int protocol, String ssid, String[] data) {
    417             this.networkId = networkId;
    418             this.protocol = protocol;
    419             this.ssid = ssid;
    420             this.data = data;
    421         }
    422 
    423         public int networkId;
    424         public int protocol;
    425         public String ssid;
    426         // EAP-SIM: data[] contains the 3 rand, one for each of the 3 challenges
    427         // EAP-AKA/AKA': data[] contains rand & authn couple for the single challenge
    428         public String[] data;
    429     }
    430 
    431     /**
    432      * The response to a SIM Auth request if successful
    433      */
    434     public static class SimAuthResponseData {
    435         public SimAuthResponseData(String type, String response) {
    436             this.type = type;
    437             this.response = response;
    438         }
    439 
    440         public String type;
    441         public String response;
    442     }
    443 
    444     public static SimAuthResponseData get3GAuthResponse(SimAuthRequestData requestData,
    445             TelephonyManager tm) {
    446         StringBuilder sb = new StringBuilder();
    447         byte[] rand = null;
    448         byte[] authn = null;
    449         String resType = WifiNative.SIM_AUTH_RESP_TYPE_UMTS_AUTH;
    450 
    451         if (requestData.data.length == 2) {
    452             try {
    453                 rand = parseHex(requestData.data[0]);
    454                 authn = parseHex(requestData.data[1]);
    455             } catch (NumberFormatException e) {
    456                 Log.e(TAG, "malformed challenge");
    457             }
    458         } else {
    459             Log.e(TAG, "malformed challenge");
    460         }
    461 
    462         String tmResponse = "";
    463         if (rand != null && authn != null) {
    464             String base64Challenge = Base64.encodeToString(concatHex(rand, authn), Base64.NO_WRAP);
    465             if (tm != null) {
    466                 tmResponse = tm.getIccAuthentication(TelephonyManager.APPTYPE_USIM,
    467                         TelephonyManager.AUTHTYPE_EAP_AKA, base64Challenge);
    468                 Log.v(TAG, "Raw Response - " + tmResponse);
    469             } else {
    470                 Log.e(TAG, "No valid TelephonyManager");
    471             }
    472         }
    473 
    474         boolean goodReponse = false;
    475         if (tmResponse != null && tmResponse.length() > 4) {
    476             byte[] result = Base64.decode(tmResponse, Base64.DEFAULT);
    477             Log.e(TAG, "Hex Response - " + makeHex(result));
    478             byte tag = result[0];
    479             if (tag == (byte) 0xdb) {
    480                 Log.v(TAG, "successful 3G authentication ");
    481                 int resLen = result[1];
    482                 String res = makeHex(result, 2, resLen);
    483                 int ckLen = result[resLen + 2];
    484                 String ck = makeHex(result, resLen + 3, ckLen);
    485                 int ikLen = result[resLen + ckLen + 3];
    486                 String ik = makeHex(result, resLen + ckLen + 4, ikLen);
    487                 sb.append(":" + ik + ":" + ck + ":" + res);
    488                 Log.v(TAG, "ik:" + ik + "ck:" + ck + " res:" + res);
    489                 goodReponse = true;
    490             } else if (tag == (byte) 0xdc) {
    491                 Log.e(TAG, "synchronisation failure");
    492                 int autsLen = result[1];
    493                 String auts = makeHex(result, 2, autsLen);
    494                 resType = WifiNative.SIM_AUTH_RESP_TYPE_UMTS_AUTS;
    495                 sb.append(":" + auts);
    496                 Log.v(TAG, "auts:" + auts);
    497                 goodReponse = true;
    498             } else {
    499                 Log.e(TAG, "bad response - unknown tag = " + tag);
    500             }
    501         } else {
    502             Log.e(TAG, "bad response - " + tmResponse);
    503         }
    504 
    505         if (goodReponse) {
    506             String response = sb.toString();
    507             Log.v(TAG, "Supplicant Response -" + response);
    508             return new SimAuthResponseData(resType, response);
    509         } else {
    510             return null;
    511         }
    512     }
    513 }
    514