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.phone.utils;
18 
19 import android.annotation.TestApi;
20 import android.content.Context;
21 import android.content.pm.PackageInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.Signature;
24 import android.telephony.Rlog;
25 import android.text.TextUtils;
26 
27 import com.android.internal.telephony.uicc.IccUtils;
28 
29 import org.json.JSONArray;
30 import org.json.JSONException;
31 import org.json.JSONObject;
32 
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.security.MessageDigest;
36 import java.security.NoSuchAlgorithmException;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Set;
42 
43 
44 public class CarrierAllowListInfo {
45     private static final String LOG_TAG = "CarrierAllowListInfo";
46     private JSONObject mDataJSON;
47     private static final String JSON_CHARSET = "UTF-8";
48     private static final String MESSAGE_DIGEST_256_ALGORITHM = "SHA-256";
49     private static final String CALLER_SHA256_ID = "callerSHA256Ids";
50     private static final String CALLER_CARRIER_ID = "carrierIds";
51     public static final int INVALID_CARRIER_ID = -1;
52 
53     private static final String CARRIER_RESTRICTION_OPERATOR_REGISTERED_FILE =
54             "CarrierRestrictionOperatorDetails.json";
55 
56     private static CarrierAllowListInfo mInstance = null;
57     private Context mContext;
58 
CarrierAllowListInfo(Context context)59     private CarrierAllowListInfo(Context context) {
60         mContext = context;
61         loadJsonFile(context);
62     }
63 
loadInstance(Context context)64     public static CarrierAllowListInfo loadInstance(Context context) {
65         if (mInstance == null) {
66             mInstance = new CarrierAllowListInfo(context);
67         }
68         return mInstance;
69     }
70 
validateCallerAndGetCarrierIds(String packageName)71     public Set<Integer> validateCallerAndGetCarrierIds(String packageName) {
72         CarrierInfo carrierInfo = parseJsonForCallerInfo(packageName);
73         boolean isValid = (carrierInfo != null) && validateCallerSignature(mContext, packageName,
74                 carrierInfo.getSHAIdList());
75         return (isValid) ? carrierInfo.getCallerCarrierIdList() : Collections.singleton(
76                 INVALID_CARRIER_ID);
77     }
78 
loadJsonFile(Context context)79     private void loadJsonFile(Context context) {
80         try {
81             String jsonString = getJsonFromAssets(context,
82                     CARRIER_RESTRICTION_OPERATOR_REGISTERED_FILE, JSON_CHARSET);
83             if (!TextUtils.isEmpty(jsonString)) {
84                 mDataJSON = new JSONObject(jsonString);
85             }
86         } catch (Exception ex) {
87             Rlog.e(LOG_TAG, "CarrierAllowListInfo: JSON file reading exception = " + ex);
88         }
89     }
90 
91     /**
92      * Parse the JSON object to fetch the given caller's SHA-Ids and carrierId.
93      */
parseJsonForCallerInfo(String callerPackage)94     private CarrierInfo parseJsonForCallerInfo(String callerPackage) {
95         try {
96             if (mDataJSON != null && callerPackage != null) {
97                 JSONObject callerJSON = mDataJSON.getJSONObject(callerPackage.trim());
98                 JSONArray callerJSONArray = callerJSON.getJSONArray(CALLER_SHA256_ID);
99                 JSONArray carrierIdArray = callerJSON.getJSONArray(CALLER_CARRIER_ID);
100 
101                 Set<Integer> carrierIds = new HashSet<>();
102                 for (int index = 0; index < carrierIdArray.length(); index++) {
103                     carrierIds.add(carrierIdArray.getInt(index));
104                 }
105 
106                 List<String> appSignatures = new ArrayList<>();
107                 for (int index = 0; index < callerJSONArray.length(); index++) {
108                     appSignatures.add((String) callerJSONArray.get(index));
109                 }
110                 return new CarrierInfo(carrierIds, appSignatures);
111             }
112         } catch (JSONException ex) {
113             Rlog.e(LOG_TAG, "getCallerSignatureInfo: JSONException = " + ex);
114         }
115         return null;
116     }
117 
118     /**
119      * Read the Json file from the assert folder.
120      *
121      * @param context  context
122      * @param fileName JSON file name in assets folder
123      * @param charset  JSON file data format
124      * @return JSON file content in string format or null in case of IOException
125      */
getJsonFromAssets(Context context, String fileName, String charset)126     private static String getJsonFromAssets(Context context, String fileName, String charset) {
127         String jsonStr;
128         try {
129             InputStream ipStream = context.getAssets().open(fileName);
130             int bufSize = ipStream.available();
131             byte[] fileBuffer = new byte[bufSize];
132             ipStream.read(fileBuffer);
133             ipStream.close();
134             jsonStr = new String(fileBuffer, charset);
135         } catch (IOException ex) {
136             Rlog.e(LOG_TAG, "getJsonFromAssets: Exception = " + ex);
137             return null;
138         }
139         return jsonStr;
140     }
141 
142     /**
143      * API fetches all the related signatures of the given package from the packageManager
144      * and validate all the signatures using SHA-256.
145      *
146      * @param context             context
147      * @param packageName         package name of the caller to validate the signatures.
148      * @param allowListSignatures list of signatures to be validated.
149      * @return {@code true} if all the signatures are available with package manager.
150      * {@code false} if any one of the signatures won't match with package manager.
151      */
validateCallerSignature(Context context, String packageName, List<String> allowListSignatures)152     public static boolean validateCallerSignature(Context context, String packageName,
153             List<String> allowListSignatures) {
154         if (TextUtils.isEmpty(packageName) || allowListSignatures.size() == 0) {
155             // package name is mandatory
156             return false;
157         }
158         final PackageManager packageManager = context.getPackageManager();
159         try {
160             MessageDigest sha256MDigest = MessageDigest.getInstance(MESSAGE_DIGEST_256_ALGORITHM);
161             final PackageInfo packageInfo = packageManager.getPackageInfo(packageName,
162                     PackageManager.GET_SIGNATURES);
163             for (Signature signature : packageInfo.signatures) {
164                 final byte[] signatureSha256 = sha256MDigest.digest(signature.toByteArray());
165                 final String hexSignatureSha256 = IccUtils.bytesToHexString(signatureSha256);
166                 if (!allowListSignatures.contains(hexSignatureSha256)) {
167                     return false;
168                 }
169             }
170             return true;
171         } catch (NoSuchAlgorithmException | PackageManager.NameNotFoundException ex) {
172             Rlog.e(LOG_TAG, "validateCallerSignature: Exception = " + ex);
173             return false;
174         }
175     }
176 
updateJsonForTest(String callerInfo)177     public int updateJsonForTest(String callerInfo) {
178         try {
179             if (callerInfo == null) {
180                 // reset the Json content after testing
181                 loadJsonFile(mContext);
182             } else {
183                 mDataJSON = new JSONObject(callerInfo);
184             }
185             return 0;
186         } catch (JSONException ex) {
187             Rlog.e(LOG_TAG, "updateJsonForTest: Exception = " + ex);
188         }
189         return -1;
190     }
191 
192     private static class CarrierInfo {
193         final private Set<Integer> mCallerCarrierIdList;
194         final private List<String> mSHAIdList;
195 
CarrierInfo(Set<Integer> carrierIds, List<String> SHAIds)196         public CarrierInfo(Set<Integer> carrierIds, List<String> SHAIds) {
197             mCallerCarrierIdList = carrierIds;
198             mSHAIdList = SHAIds;
199         }
200 
getCallerCarrierIdList()201         public Set<Integer> getCallerCarrierIdList() {
202             return mCallerCarrierIdList;
203         }
204 
getSHAIdList()205         public List<String> getSHAIdList() {
206             return mSHAIdList;
207         }
208     }
209 
210     @TestApi
getShaIdList(String srcPkg, int carrierId)211     public List<String> getShaIdList(String srcPkg, int carrierId) {
212         CarrierInfo carrierInfo = parseJsonForCallerInfo(srcPkg);
213         if (carrierInfo != null && carrierInfo.getCallerCarrierIdList().contains(carrierId)) {
214             return carrierInfo.getSHAIdList();
215         }
216         Rlog.e(LOG_TAG, "getShaIdList: carrierId or shaIdList is empty");
217         return Collections.EMPTY_LIST;
218     }
219 }
220