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