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.safetycenter.data;
18 
19 import android.annotation.UserIdInt;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.content.pm.Signature;
23 import android.safetycenter.SafetySourceData;
24 import android.safetycenter.SafetySourceIssue;
25 import android.safetycenter.SafetySourceStatus;
26 import android.safetycenter.config.SafetySource;
27 import android.util.Log;
28 
29 import androidx.annotation.Nullable;
30 
31 import com.android.modules.utils.build.SdkLevel;
32 import com.android.safetycenter.SafetyCenterConfigReader;
33 import com.android.safetycenter.SafetyCenterFlags;
34 import com.android.safetycenter.SafetySources;
35 import com.android.safetycenter.UserProfileGroup;
36 
37 import java.util.List;
38 import java.util.Set;
39 
40 import javax.annotation.concurrent.NotThreadSafe;
41 
42 /**
43  * Validates calls made to the Safety Center API to get, set or clear {@link SafetySourceData}, or
44  * to report an error.
45  *
46  * <p>This class isn't thread safe. Thread safety must be handled by the caller.
47  */
48 @NotThreadSafe
49 final class SafetySourceDataValidator {
50 
51     private static final String TAG = "SafetySourceDataValidat";
52 
53     private final Context mContext;
54     private final SafetyCenterConfigReader mSafetyCenterConfigReader;
55     private final PackageManager mPackageManager;
56 
SafetySourceDataValidator(Context context, SafetyCenterConfigReader safetyCenterConfigReader)57     SafetySourceDataValidator(Context context, SafetyCenterConfigReader safetyCenterConfigReader) {
58         mContext = context;
59         mSafetyCenterConfigReader = safetyCenterConfigReader;
60         mPackageManager = mContext.getPackageManager();
61     }
62 
63     /**
64      * Validates a call to the Safety Center API, from the given {@code packageName} and {@code
65      * userId} to get, set or clear {@link SafetySourceData}, or to report an error, for the given
66      * {@code safetySourceId}. Returns {@code true} if the call is valid and should proceed, or
67      * {@code false} otherwise.
68      *
69      * <p>This method may throw an {@link IllegalArgumentException} in some invalid cases.
70      *
71      * @param safetySourceData being set, or {@code null} if retrieving or clearing data, or
72      *     reporting an error
73      * @param callerCanAccessAnySource whether we should allow the caller to access any source, or
74      *     restrict them to their own {@code packageName}
75      */
validateRequest( @ullable SafetySourceData safetySourceData, boolean callerCanAccessAnySource, String safetySourceId, String packageName, @UserIdInt int userId)76     boolean validateRequest(
77             @Nullable SafetySourceData safetySourceData,
78             boolean callerCanAccessAnySource,
79             String safetySourceId,
80             String packageName,
81             @UserIdInt int userId) {
82         SafetyCenterConfigReader.ExternalSafetySource externalSafetySource =
83                 mSafetyCenterConfigReader.getExternalSafetySource(safetySourceId, packageName);
84         if (externalSafetySource == null) {
85             throw new IllegalArgumentException("Unexpected safety source: " + safetySourceId);
86         }
87 
88         SafetySource safetySource = externalSafetySource.getSafetySource();
89         if (!callerCanAccessAnySource) {
90             validateCallingPackage(safetySource, packageName, safetySourceId);
91         }
92 
93         @UserProfileGroup.ProfileType int profileType =
94                 UserProfileGroup.getProfileTypeOfUser(userId, mContext);
95         if (!SafetySources.supportsProfileType(safetySource, profileType)) {
96             throw new IllegalArgumentException(
97                     "Unexpected profile type: "
98                             + profileType
99                             + " for safety source: "
100                             + safetySourceId);
101         }
102 
103         boolean retrievingOrClearingData = safetySourceData == null;
104         if (retrievingOrClearingData) {
105             return isExternalSafetySourceActive(
106                     callerCanAccessAnySource, safetySourceId, packageName);
107         }
108 
109         SafetySourceStatus safetySourceStatus = safetySourceData.getStatus();
110 
111         if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY
112                 && safetySourceStatus != null) {
113             throw new IllegalArgumentException(
114                     "Unexpected status for issue only safety source: " + safetySourceId);
115         }
116 
117         if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC
118                 && safetySource.getInitialDisplayState()
119                         != SafetySource.INITIAL_DISPLAY_STATE_HIDDEN
120                 && safetySourceStatus == null) {
121             throw new IllegalArgumentException(
122                     "Missing status for dynamic safety source: " + safetySourceId);
123         }
124 
125         if (safetySourceStatus != null) {
126             int sourceSeverityLevel = safetySourceStatus.getSeverityLevel();
127 
128             if (externalSafetySource.hasEntryInStatelessGroup()
129                     && sourceSeverityLevel != SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED) {
130                 throw new IllegalArgumentException(
131                         "Safety source: "
132                                 + safetySourceId
133                                 + " is in a stateless group but specified a severity level: "
134                                 + sourceSeverityLevel);
135             }
136 
137             int maxSourceSeverityLevel =
138                     Math.max(
139                             SafetySourceData.SEVERITY_LEVEL_INFORMATION,
140                             safetySource.getMaxSeverityLevel());
141 
142             if (sourceSeverityLevel > maxSourceSeverityLevel) {
143                 throw new IllegalArgumentException(
144                         "Unexpected severity level: "
145                                 + sourceSeverityLevel
146                                 + ", for safety source: "
147                                 + safetySourceId);
148             }
149         }
150 
151         List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();
152 
153         for (int i = 0; i < safetySourceIssues.size(); i++) {
154             SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
155             int issueSeverityLevel = safetySourceIssue.getSeverityLevel();
156             if (issueSeverityLevel > safetySource.getMaxSeverityLevel()) {
157                 throw new IllegalArgumentException(
158                         "Unexpected severity level: "
159                                 + issueSeverityLevel
160                                 + ", for issue in safety source: "
161                                 + safetySourceId);
162             }
163 
164             int issueCategory = safetySourceIssue.getIssueCategory();
165             if (!SafetyCenterFlags.isIssueCategoryAllowedForSource(issueCategory, safetySourceId)) {
166                 throw new IllegalArgumentException(
167                         "Unexpected issue category: "
168                                 + issueCategory
169                                 + ", for issue in safety source: "
170                                 + safetySourceId);
171             }
172         }
173 
174         return isExternalSafetySourceActive(callerCanAccessAnySource, safetySourceId, packageName);
175     }
176 
isExternalSafetySourceActive( boolean callerCanAccessAnySource, String safetySourceId, String callerPackageName)177     private boolean isExternalSafetySourceActive(
178             boolean callerCanAccessAnySource, String safetySourceId, String callerPackageName) {
179         boolean isActive =
180                 mSafetyCenterConfigReader.isExternalSafetySourceActive(
181                         safetySourceId, callerCanAccessAnySource ? null : callerPackageName);
182         if (!isActive) {
183             Log.i(
184                     TAG,
185                     "Call ignored as safety source " + safetySourceId + " is not currently active");
186         }
187         return isActive;
188     }
189 
validateCallingPackage( SafetySource safetySource, String packageName, String safetySourceId)190     private void validateCallingPackage(
191             SafetySource safetySource, String packageName, String safetySourceId) {
192         if (!packageName.equals(safetySource.getPackageName())) {
193             throw new IllegalArgumentException(
194                     "Unexpected package name: "
195                             + packageName
196                             + ", for safety source: "
197                             + safetySourceId);
198         }
199 
200         if (!SdkLevel.isAtLeastU()) {
201             // No more validation checks possible on T devices
202             return;
203         }
204 
205         Set<String> certificateHashes = safetySource.getPackageCertificateHashes();
206         if (certificateHashes.isEmpty()) {
207             Log.d(TAG, "No cert check requested for package " + packageName);
208             return;
209         }
210 
211         if (!checkCerts(packageName, certificateHashes)
212                 && !checkCerts(
213                         packageName,
214                         SafetyCenterFlags.getAdditionalAllowedPackageCerts(packageName))) {
215             Log.w(
216                     TAG,
217                     "Package: "
218                             + packageName
219                             + ", for source: "
220                             + safetySourceId
221                             + " is signed with invalid signature");
222             throw new IllegalArgumentException("Invalid signature for package " + packageName);
223         }
224     }
225 
checkCerts(String packageName, Set<String> certificateHashes)226     private boolean checkCerts(String packageName, Set<String> certificateHashes) {
227         boolean hasMatchingCert = false;
228         for (String certHash : certificateHashes) {
229             try {
230                 byte[] certificate = new Signature(certHash).toByteArray();
231                 if (mPackageManager.hasSigningCertificate(
232                         packageName, certificate, PackageManager.CERT_INPUT_SHA256)) {
233                     Log.v(TAG, "Package: " + packageName + " has expected signature");
234                     hasMatchingCert = true;
235                 }
236             } catch (IllegalArgumentException e) {
237                 Log.w(TAG, "Failed to parse signing certificate: " + certHash, e);
238                 throw new IllegalStateException(
239                         "Failed to parse signing certificate: " + certHash, e);
240             }
241         }
242         return hasMatchingCert;
243     }
244 }
245