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