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