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 package com.android.server.wifi.entitlement;
17 
18 import static java.nio.charset.StandardCharsets.UTF_8;
19 
20 import android.os.Build;
21 import android.telephony.TelephonyManager;
22 import android.text.TextUtils;
23 import android.util.Base64;
24 
25 import androidx.annotation.Nullable;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import org.json.JSONArray;
30 import org.json.JSONException;
31 import org.json.JSONObject;
32 
33 /** Creates the request to do authentication and query the entitlement status. */
34 public class RequestFactory {
35     private static final int IMEI_LENGTH = 14;
36 
37     @VisibleForTesting
38     static final String METHOD_3GPP_AUTHENTICATION = "3gppAuthentication";
39     @VisibleForTesting
40     static final String METHOD_GET_IMSI_PSEUDONYM = "getImsiPseudonym";
41     @VisibleForTesting
42     static final String JSON_KEY_MESSAGE_ID = "message-id";
43     @VisibleForTesting
44     static final String JSON_KEY_METHOD = "method";
45     @VisibleForTesting
46     static final String JSON_KEY_DEVICE_ID = "device-id";
47     @VisibleForTesting
48     static final String JSON_KEY_DEVICE_TYPE = "device-type";
49     @VisibleForTesting
50     static final String JSON_KEY_OS_TYPE = "os-type";
51     @VisibleForTesting
52     static final String JSON_KEY_DEVICE_NAME = "device-name";
53     @VisibleForTesting
54     static final String JSON_KEY_IMSI_EAP = "imsi-eap";
55     @VisibleForTesting
56     static final String JSON_KEY_AKA_TOKEN = "aka-token";
57     @VisibleForTesting
58     static final String JSON_KEY_AKA_CHALLENGE_RSP = "aka-challenge-rsp";
59 
60     @VisibleForTesting
61     static final int DEVICE_TYPE_SIM = 0;
62     @VisibleForTesting
63     static final int OS_TYPE_ANDROID = 0;
64 
65     public static final int MESSAGE_ID_3GPP_AUTHENTICATION = 1;
66     public static final int MESSAGE_ID_GET_IMSI_PSEUDONYM = 2;
67 
68     private final TelephonyManager mTelephonyManager;
69 
RequestFactory(TelephonyManager telephonyManager)70     public RequestFactory(TelephonyManager telephonyManager) {
71         mTelephonyManager = telephonyManager;
72     }
73 
74     /**
75      * Creates {@link JSONArray} object with method {@code METHOD_3GPP_AUTHENTICATION} to get
76      * challenge response data.
77      */
createAuthRequest()78     public JSONArray createAuthRequest() throws TransientException {
79         JSONArray requests = new JSONArray();
80         try {
81             requests.put(makeAuthenticationRequest(null /*akaToken*/, null /*challengeResponse*/));
82         } catch (JSONException e) {
83             throw new TransientException("createAuthRequest failed!" + e);
84         }
85         return requests;
86     }
87 
88     /**
89      * Creates {link JSONArray} object with method {@code METHOD_SERVICE_ENTITLEMENT_STATUS} to
90      * query entitlement status.
91      */
createGetImsiPseudonymRequest(String akaToken, String challengeResponse)92     public JSONArray createGetImsiPseudonymRequest(String akaToken, String challengeResponse)
93             throws TransientException {
94         JSONArray requests = new JSONArray();
95         try {
96             requests.put(makeAuthenticationRequest(akaToken, challengeResponse));
97             requests.put(makeGetImsiPseudonymRequest());
98         } catch (JSONException e) {
99             throw new TransientException("createGetImsiPseudonymRequest failed!" + e);
100         }
101         return requests;
102     }
103 
makeBaseRequest(int messageId, String method)104     private JSONObject makeBaseRequest(int messageId, String method) throws JSONException {
105         JSONObject request = new JSONObject();
106         request.put(JSON_KEY_MESSAGE_ID, messageId);
107         request.put(JSON_KEY_METHOD, method);
108         return request;
109     }
110 
111     @Nullable
getImeiSv()112     private String getImeiSv() {
113         String imeiValue = mTelephonyManager.getImei();
114         String svnValue = mTelephonyManager.getDeviceSoftwareVersion();
115         if (TextUtils.isEmpty(imeiValue) || TextUtils.isEmpty(svnValue)) {
116             return null;
117         }
118         if (imeiValue.length() > IMEI_LENGTH) {
119             imeiValue = imeiValue.substring(0, IMEI_LENGTH);
120         }
121         String value = imeiValue + svnValue; // 14 digits + 2 digits
122         return Base64.encodeToString(value.getBytes(UTF_8), Base64.NO_WRAP).trim();
123     }
124 
makeAuthenticationRequest(String akaToken, String challengeResponse)125     private JSONObject makeAuthenticationRequest(String akaToken, String challengeResponse)
126             throws JSONException, TransientException {
127         JSONObject request =
128                 makeBaseRequest(MESSAGE_ID_3GPP_AUTHENTICATION, METHOD_3GPP_AUTHENTICATION);
129         String imeiSv = getImeiSv();
130         if (imeiSv == null) {
131             // device-id(base64 encodede IMEISV) is mandatory.
132             throw new TransientException("IMEISV is null.");
133         }
134         request.put(JSON_KEY_DEVICE_ID, imeiSv);
135         request.put(JSON_KEY_DEVICE_TYPE, DEVICE_TYPE_SIM);
136         request.put(JSON_KEY_OS_TYPE, OS_TYPE_ANDROID);
137         request.put(JSON_KEY_DEVICE_NAME, Build.MODEL);
138         request.put(JSON_KEY_IMSI_EAP, getImsiEap());
139         if (!TextUtils.isEmpty(akaToken)) {
140             request.put(JSON_KEY_AKA_TOKEN, akaToken);
141         }
142         if (!TextUtils.isEmpty(challengeResponse)) {
143             request.put(JSON_KEY_AKA_CHALLENGE_RSP, challengeResponse);
144         }
145         return request;
146     }
147 
148     @Nullable
getImsiEap()149     private String getImsiEap() {
150         String imsi = mTelephonyManager.getSubscriberId();
151         String mccmnc = mTelephonyManager.getSimOperator(); // MCCMNC is 5 or 6 decimal digits
152         if ((imsi == null) || (mccmnc == null) || (mccmnc.length() < 5)) {
153             return null;
154         }
155         String mcc = mccmnc.substring(0, 3);
156         String mnc = mccmnc.substring(3);
157         return String.format("0%s@nai.epc.mnc%s.mcc%s.3gppnetwork.org", imsi, mnc, mcc);
158     }
159 
makeGetImsiPseudonymRequest()160     private JSONObject makeGetImsiPseudonymRequest() throws JSONException {
161         return makeBaseRequest(MESSAGE_ID_GET_IMSI_PSEUDONYM, METHOD_GET_IMSI_PSEUDONYM);
162     }
163 }
164 
165