1 /* 2 * Copyright (C) 2023 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.entitlement; 18 19 import static com.android.libraries.entitlement.EapAkaHelper.EapAkaResponse; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.content.Context; 24 import android.os.Handler; 25 import android.telephony.TelephonyManager; 26 import android.text.TextUtils; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.libraries.entitlement.EapAkaHelper; 30 import com.android.libraries.entitlement.ServiceEntitlementException; 31 import com.android.modules.utils.BackgroundThread; 32 import com.android.server.wifi.entitlement.http.HttpClient; 33 import com.android.server.wifi.entitlement.http.HttpConstants.RequestMethod; 34 import com.android.server.wifi.entitlement.http.HttpRequest; 35 import com.android.server.wifi.entitlement.http.HttpResponse; 36 import com.android.server.wifi.entitlement.response.ChallengeResponse; 37 import com.android.server.wifi.entitlement.response.GetImsiPseudonymResponse; 38 import com.android.server.wifi.entitlement.response.Response; 39 40 import com.google.common.net.HttpHeaders; 41 42 import java.lang.annotation.Retention; 43 import java.lang.annotation.RetentionPolicy; 44 import java.net.MalformedURLException; 45 import java.net.URL; 46 import java.util.Optional; 47 48 /** 49 * Implements the protocol to get IMSI pseudonym from service entitlement server. 50 */ 51 public class CarrierSpecificServiceEntitlement { 52 public static final int REASON_HTTPS_CONNECTION_FAILURE = 0; 53 public static final int REASON_TRANSIENT_FAILURE = 1; 54 public static final int REASON_NON_TRANSIENT_FAILURE = 2; 55 public static final String[] FAILURE_REASON_NAME = { 56 "HTTP connection failure", 57 "Transient failure", 58 "Non-transient failure", 59 }; 60 @IntDef(prefix = { "REASON_" }, value = { 61 REASON_HTTPS_CONNECTION_FAILURE, 62 REASON_TRANSIENT_FAILURE, 63 REASON_NON_TRANSIENT_FAILURE, 64 }) 65 @Retention(RetentionPolicy.SOURCE) 66 public @interface FailureReasonCode {} 67 68 private static final String MIME_TYPE_JSON = "application/json"; 69 private static final String ENCODING_GZIP = "gzip"; 70 private static final int CONNECT_TIMEOUT_SECS = 30; 71 72 private final RequestFactory mRequestFactory; 73 private final HttpRequest.Builder mHttpRequestBuilder; 74 private final EapAkaHelper mEapAkaHelper; 75 private final String mImsi; 76 private final Handler mBackgroundHandler; 77 78 private String mAkaTokenCache; 79 CarrierSpecificServiceEntitlement(@onNull Context context, int subId, @NonNull String serverUrl)80 public CarrierSpecificServiceEntitlement(@NonNull Context context, int subId, 81 @NonNull String serverUrl) throws MalformedURLException { 82 this(context.getSystemService(TelephonyManager.class).createForSubscriptionId(subId), 83 EapAkaHelper.getInstance(context, subId), serverUrl); 84 } 85 CarrierSpecificServiceEntitlement(@onNull TelephonyManager telephonyManager, @NonNull EapAkaHelper eapAkaHelper, @NonNull String serverUrl)86 private CarrierSpecificServiceEntitlement(@NonNull TelephonyManager telephonyManager, 87 @NonNull EapAkaHelper eapAkaHelper, @NonNull String serverUrl) 88 throws MalformedURLException { 89 this(telephonyManager.getSubscriberId(), new RequestFactory(telephonyManager), eapAkaHelper, 90 serverUrl, BackgroundThread.getHandler()); 91 } 92 93 @VisibleForTesting CarrierSpecificServiceEntitlement(@onNull String imsi, @NonNull RequestFactory requestFactory, @NonNull EapAkaHelper eapAkaHelper, @NonNull String serverUrl, @NonNull Handler backgroundHandler)94 CarrierSpecificServiceEntitlement(@NonNull String imsi, 95 @NonNull RequestFactory requestFactory, 96 @NonNull EapAkaHelper eapAkaHelper, 97 @NonNull String serverUrl, 98 @NonNull Handler backgroundHandler) throws MalformedURLException { 99 URL url = new URL(serverUrl); 100 if (!TextUtils.equals(url.getProtocol(), "https")) { 101 throw new MalformedURLException("The server URL must use HTTPS protocol"); 102 } 103 mImsi = imsi; 104 mRequestFactory = requestFactory; 105 mHttpRequestBuilder = HttpRequest.builder() 106 .setUrl(serverUrl) 107 .setRequestMethod(RequestMethod.POST) 108 .addRequestProperty(HttpHeaders.CONTENT_TYPE, MIME_TYPE_JSON) 109 .addRequestProperty(HttpHeaders.CONTENT_ENCODING, ENCODING_GZIP) 110 .addRequestProperty(HttpHeaders.ACCEPT, MIME_TYPE_JSON) 111 .setTimeoutInSec(CONNECT_TIMEOUT_SECS); 112 mEapAkaHelper = eapAkaHelper; 113 mBackgroundHandler = backgroundHandler; 114 } 115 116 /** 117 * Retrieve the OOB IMSI pseudonym from the entitlement server in the BackgroundThread. 118 * 119 * @param callbackHandler The handler used to run the callback. 120 * @param callback The callback which will be called when the pseudonym is retrieved from 121 * server. 122 */ getImsiPseudonym(int carrierId, @NonNull Handler callbackHandler, @NonNull Callback callback)123 public void getImsiPseudonym(int carrierId, @NonNull Handler callbackHandler, 124 @NonNull Callback callback) { 125 mBackgroundHandler.post(() -> { 126 try { 127 Optional<PseudonymInfo> optionalPseudonymInfo = getImsiPseudonym(); 128 if (optionalPseudonymInfo.isPresent()) { 129 callbackHandler.post(() -> callback.onSuccess(carrierId, 130 optionalPseudonymInfo.get())); 131 } else { 132 callbackHandler.post(() -> callback.onFailure(carrierId, 133 REASON_NON_TRANSIENT_FAILURE, "No valid pseudonym is received")); 134 } 135 } catch (ServiceEntitlementException e) { 136 callbackHandler.post(() -> callback.onFailure(carrierId, 137 REASON_HTTPS_CONNECTION_FAILURE, e.toString())); 138 } catch (TransientException e) { 139 callbackHandler.post(() -> callback.onFailure(carrierId, REASON_TRANSIENT_FAILURE, 140 e.toString())); 141 } catch (NonTransientException e) { 142 callbackHandler.post(() -> callback.onFailure(carrierId, 143 REASON_NON_TRANSIENT_FAILURE, e.toString())); 144 } 145 }); 146 } 147 148 /** 149 * Retrieve the OOB IMSI pseudonym from the entitlement server. 150 * @throws TransientException if a transient failure like failure to connect with server or 151 * server's temporary problem etc. 152 * @throws NonTransientException if a non-transient failure, like failure to get challenge 153 * response or authentication failure from server etc. 154 * @throws ServiceEntitlementException if there is any HTTPS connection failure. 155 */ getImsiPseudonym()156 private Optional<PseudonymInfo> getImsiPseudonym() throws 157 TransientException, NonTransientException, ServiceEntitlementException { 158 String eapAkaChallenge = null; 159 if (TextUtils.isEmpty(mAkaTokenCache)) { 160 eapAkaChallenge = getAuthenticationChallenge(); 161 } 162 String eapAkaChallengeResponse = null; 163 if (!TextUtils.isEmpty(eapAkaChallenge)) { 164 EapAkaResponse eapAkaResponse = mEapAkaHelper.getEapAkaResponse(eapAkaChallenge); 165 if (eapAkaResponse == null) { 166 throw new NonTransientException("Can't get the AKA challenge response."); 167 } 168 eapAkaChallengeResponse = eapAkaResponse.response(); 169 if (eapAkaChallengeResponse == null) { 170 throw new TransientException("EAP-AKA Challenge message not valid!"); 171 } 172 } 173 174 HttpResponse httpResponse = HttpClient.request( 175 mHttpRequestBuilder.setPostDataJsonArray( 176 mRequestFactory.createGetImsiPseudonymRequest( 177 mAkaTokenCache, eapAkaChallengeResponse)).build()); 178 179 GetImsiPseudonymResponse imsiPseudonymResponse = 180 new GetImsiPseudonymResponse(httpResponse.body()); 181 int authResponseCode = imsiPseudonymResponse.getAuthResponseCode(); 182 switch (authResponseCode) { 183 case Response.RESPONSE_CODE_REQUEST_SUCCESSFUL: 184 // only save AKA token for full authentication 185 if (!TextUtils.isEmpty(eapAkaChallengeResponse) 186 && !TextUtils.isEmpty(imsiPseudonymResponse.getAkaToken())) { 187 mAkaTokenCache = imsiPseudonymResponse.getAkaToken(); 188 } 189 break; 190 case Response.RESPONSE_CODE_AKA_CHALLENGE: 191 if (mAkaTokenCache == null) { 192 throw new TransientException("Something is wrong in the server side."); 193 } 194 // clear AKA token to trigger full authentication next time 195 mAkaTokenCache = null; 196 // TODO(b/274167498): Optimize the handling of expired AKA token 197 return getImsiPseudonym(); 198 case Response.RESPONSE_CODE_AKA_AUTH_FAILED: 199 throw new NonTransientException("Authentication failed!"); 200 case Response.RESPONSE_CODE_INVALID_REQUEST: 201 throw new NonTransientException("Invalid request!"); 202 case Response.RESPONSE_CODE_SERVER_ERROR: 203 throw new TransientException("Server error!"); 204 default: 205 throw new NonTransientException("Unknown error!"); 206 } 207 208 int imsiPseudonymResponseCode = imsiPseudonymResponse.getGetImsiPseudonymResponseCode(); 209 switch (imsiPseudonymResponseCode) { 210 case Response.RESPONSE_CODE_REQUEST_SUCCESSFUL: 211 break; 212 213 /* 214 * As experience, server may respond 1004(RESPONSE_CODE_INVALID_REQUEST) if it detects 215 * the secondary request not going to the same server with the first initial request, 216 * retry to recover it. 217 */ 218 case Response.RESPONSE_CODE_INVALID_REQUEST: 219 case Response.RESPONSE_CODE_SERVER_ERROR: 220 case Response.RESPONSE_CODE_3GPP_AUTH_ONGOING: 221 throw new TransientException("Server transient problem! Response code is " 222 + imsiPseudonymResponseCode); 223 224 case Response.RESPONSE_CODE_FORBIDDEN_REQUEST: 225 case Response.RESPONSE_CODE_UNSUPPORTED_OPERATION: 226 default: 227 throw new NonTransientException("Something wrong when getting IMSI pseudonym! " 228 + "Response code is " + imsiPseudonymResponseCode); 229 } 230 return imsiPseudonymResponse.toPseudonymInfo(mImsi); 231 } 232 getAuthenticationChallenge()233 private String getAuthenticationChallenge() 234 throws TransientException, NonTransientException, ServiceEntitlementException { 235 HttpResponse httpResponse = 236 HttpClient.request( 237 mHttpRequestBuilder.setPostDataJsonArray( 238 mRequestFactory.createAuthRequest()).build()); 239 ChallengeResponse challengeResponse = new ChallengeResponse(httpResponse.body()); 240 int authResponseCode = challengeResponse.getAuthResponseCode(); 241 switch (authResponseCode) { 242 case Response.RESPONSE_CODE_AKA_CHALLENGE: 243 break; 244 245 /* 246 * As experience, server may respond 1004(RESPONSE_CODE_INVALID_REQUEST) if it detects 247 * the secondary request not going to the same server with the first initial request, 248 * retry to recover it. 249 */ 250 case Response.RESPONSE_CODE_INVALID_REQUEST: 251 case Response.RESPONSE_CODE_SERVER_ERROR: 252 throw new TransientException("Server transient problem! Response code is " 253 + authResponseCode); 254 255 case Response.RESPONSE_CODE_AKA_AUTH_FAILED: 256 case Response.RESPONSE_CODE_REQUEST_SUCCESSFUL: 257 default: 258 throw new NonTransientException( 259 "Something wrong when getting authentication challenge! authResponseCode=" 260 + authResponseCode); 261 } 262 return challengeResponse.getEapAkaChallenge(); 263 } 264 265 /** 266 * Callback which will be called after OOB pseudonym retrieval. 267 */ 268 public interface Callback { 269 270 /** 271 * Indicates an OOB pseudonym have been retrieved successfully. 272 * @param carrierId The target carrier ID of the retrieved OOB pseudonym. 273 * @param pseudonymInfo The retrieved OOB pseudonym info. 274 */ onSuccess(int carrierId, PseudonymInfo pseudonymInfo)275 void onSuccess(int carrierId, PseudonymInfo pseudonymInfo); 276 277 /** 278 * Indicate a failure happens when to retrieve the OOB pseudonym. 279 * @param carrierId The target carrier ID of the retrieval failure. 280 * @param reasonCode The failure reason code 281 * @param description The description of the failure. 282 */ onFailure(int carrierId, @FailureReasonCode int reasonCode, String description)283 void onFailure(int carrierId, @FailureReasonCode int reasonCode, String description); 284 } 285 } 286 287