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