1 /* 2 * Copyright (C) 2022 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 android.content.pm.parsing; 18 19 import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; 20 21 import android.annotation.CheckResult; 22 import android.annotation.IntRange; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.content.pm.PackageManager; 26 import android.content.pm.Signature; 27 import android.content.pm.SigningDetails; 28 import android.content.pm.parsing.result.ParseInput; 29 import android.content.pm.parsing.result.ParseResult; 30 import android.os.Build; 31 import android.os.FileUtils; 32 import android.os.SystemProperties; 33 import android.text.TextUtils; 34 import android.util.Base64; 35 import android.util.Slog; 36 import android.util.apk.ApkSignatureVerifier; 37 38 import com.android.internal.util.ArrayUtils; 39 import com.android.modules.utils.build.UnboundedSdkLevel; 40 41 import java.security.KeyFactory; 42 import java.security.NoSuchAlgorithmException; 43 import java.security.PublicKey; 44 import java.security.spec.EncodedKeySpec; 45 import java.security.spec.InvalidKeySpecException; 46 import java.security.spec.X509EncodedKeySpec; 47 import java.util.Arrays; 48 49 /** @hide */ 50 public class FrameworkParsingPackageUtils { 51 52 private static final String TAG = "FrameworkParsingPackageUtils"; 53 54 /** 55 * For those names would be used as a part of the file name. Limits size to 223 and reserves 32 56 * for the OS. 57 */ 58 private static final int MAX_FILE_NAME_SIZE = 223; 59 60 public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7; 61 public static final int PARSE_APK_IN_APEX = 1 << 9; 62 63 /** 64 * Check if the given name is valid. 65 * 66 * @param name The name to check. 67 * @param requireSeparator {@code true} if the name requires containing a separator at least. 68 * @param requireFilename {@code true} to apply file name validation to the given name. It also 69 * limits length of the name to the {@link #MAX_FILE_NAME_SIZE}. 70 * @return Success if it's valid. 71 */ validateName(String name, boolean requireSeparator, boolean requireFilename)72 public static String validateName(String name, boolean requireSeparator, 73 boolean requireFilename) { 74 final int N = name.length(); 75 boolean hasSep = false; 76 boolean front = true; 77 for (int i = 0; i < N; i++) { 78 final char c = name.charAt(i); 79 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { 80 front = false; 81 continue; 82 } 83 if (!front) { 84 if ((c >= '0' && c <= '9') || c == '_') { 85 continue; 86 } 87 } 88 if (c == '.') { 89 hasSep = true; 90 front = true; 91 continue; 92 } 93 return "bad character '" + c + "'"; 94 } 95 if (requireFilename) { 96 if (!FileUtils.isValidExtFilename(name)) { 97 return "Invalid filename"; 98 } else if (N > MAX_FILE_NAME_SIZE) { 99 return "the length of the name is greater than " + MAX_FILE_NAME_SIZE; 100 } 101 } 102 return hasSep || !requireSeparator ? null : "must have at least one '.' separator"; 103 } 104 105 /** 106 * @see #validateName(String, boolean, boolean) 107 */ validateName(ParseInput input, String name, boolean requireSeparator, boolean requireFilename)108 public static ParseResult validateName(ParseInput input, String name, boolean requireSeparator, 109 boolean requireFilename) { 110 final String errorMessage = validateName(name, requireSeparator, requireFilename); 111 if (errorMessage != null) { 112 return input.error(errorMessage); 113 } 114 return input.success(null); 115 } 116 117 /** 118 * @return {@link PublicKey} of a given encoded public key. 119 */ parsePublicKey(final String encodedPublicKey)120 public static PublicKey parsePublicKey(final String encodedPublicKey) { 121 if (encodedPublicKey == null) { 122 Slog.w(TAG, "Could not parse null public key"); 123 return null; 124 } 125 126 try { 127 return parsePublicKey(Base64.decode(encodedPublicKey, Base64.DEFAULT)); 128 } catch (IllegalArgumentException e) { 129 Slog.w(TAG, "Could not parse verifier public key; invalid Base64"); 130 return null; 131 } 132 } 133 134 /** 135 * @return {@link PublicKey} of the given byte array of a public key. 136 */ parsePublicKey(final byte[] publicKey)137 public static PublicKey parsePublicKey(final byte[] publicKey) { 138 if (publicKey == null) { 139 Slog.w(TAG, "Could not parse null public key"); 140 return null; 141 } 142 143 final EncodedKeySpec keySpec; 144 try { 145 keySpec = new X509EncodedKeySpec(publicKey); 146 } catch (IllegalArgumentException e) { 147 Slog.w(TAG, "Could not parse verifier public key; invalid Base64"); 148 return null; 149 } 150 151 /* First try the key as an RSA key. */ 152 try { 153 final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 154 return keyFactory.generatePublic(keySpec); 155 } catch (NoSuchAlgorithmException e) { 156 Slog.wtf(TAG, "Could not parse public key: RSA KeyFactory not included in build"); 157 } catch (InvalidKeySpecException e) { 158 // Not a RSA public key. 159 } 160 161 /* Now try it as a ECDSA key. */ 162 try { 163 final KeyFactory keyFactory = KeyFactory.getInstance("EC"); 164 return keyFactory.generatePublic(keySpec); 165 } catch (NoSuchAlgorithmException e) { 166 Slog.wtf(TAG, "Could not parse public key: EC KeyFactory not included in build"); 167 } catch (InvalidKeySpecException e) { 168 // Not a ECDSA public key. 169 } 170 171 /* Now try it as a DSA key. */ 172 try { 173 final KeyFactory keyFactory = KeyFactory.getInstance("DSA"); 174 return keyFactory.generatePublic(keySpec); 175 } catch (NoSuchAlgorithmException e) { 176 Slog.wtf(TAG, "Could not parse public key: DSA KeyFactory not included in build"); 177 } catch (InvalidKeySpecException e) { 178 // Not a DSA public key. 179 } 180 181 /* Not a supported key type */ 182 return null; 183 } 184 185 /** 186 * Returns {@code true} if both the property name and value are empty or if the given system 187 * property is set to the specified value. Properties can be one or more, and if properties are 188 * more than one, they must be separated by comma, and count of names and values must be equal, 189 * and also every given system property must be set to the corresponding value. 190 * In all other cases, returns {@code false} 191 */ checkRequiredSystemProperties(@ullable String rawPropNames, @Nullable String rawPropValues)192 public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames, 193 @Nullable String rawPropValues) { 194 if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) { 195 if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) { 196 // malformed condition - incomplete 197 Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames 198 + "=" + rawPropValues + "' - require both requiredSystemPropertyName" 199 + " AND requiredSystemPropertyValue to be specified."); 200 return false; 201 } 202 // no valid condition set - so no exclusion criteria, overlay will be included. 203 return true; 204 } 205 206 final String[] propNames = rawPropNames.split(","); 207 final String[] propValues = rawPropValues.split(","); 208 209 if (propNames.length != propValues.length) { 210 Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames 211 + "=" + rawPropValues + "' - require both requiredSystemPropertyName" 212 + " AND requiredSystemPropertyValue lists to have the same size."); 213 return false; 214 } 215 for (int i = 0; i < propNames.length; i++) { 216 // Check property value: make sure it is both set and equal to expected value 217 final String currValue = SystemProperties.get(propNames[i]); 218 if (!TextUtils.equals(currValue, propValues[i])) { 219 return false; 220 } 221 } 222 return true; 223 } 224 225 @CheckResult getSigningDetails(ParseInput input, String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary, @NonNull SigningDetails existingSigningDetails, int targetSdk)226 public static ParseResult<SigningDetails> getSigningDetails(ParseInput input, 227 String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary, 228 @NonNull SigningDetails existingSigningDetails, int targetSdk) { 229 int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( 230 targetSdk); 231 if (isStaticSharedLibrary) { 232 // must use v2 signing scheme 233 minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2; 234 } 235 final ParseResult<SigningDetails> verified; 236 if (skipVerify) { 237 // systemDir APKs are already trusted, save time by not verifying; since the 238 // signature is not verified and some system apps can have their V2+ signatures 239 // stripped allow pulling the certs from the jar signature. 240 verified = ApkSignatureVerifier.unsafeGetCertsWithoutVerification(input, baseCodePath, 241 SigningDetails.SignatureSchemeVersion.JAR); 242 } else { 243 verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme); 244 } 245 246 if (verified.isError()) { 247 return input.error(verified); 248 } 249 250 // Verify that entries are signed consistently with the first pkg 251 // we encountered. Note that for splits, certificates may have 252 // already been populated during an earlier parse of a base APK. 253 if (existingSigningDetails == SigningDetails.UNKNOWN) { 254 return verified; 255 } else { 256 if (!Signature.areExactMatch(existingSigningDetails, 257 verified.getResult())) { 258 return input.error(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, 259 baseCodePath + " has mismatched certificates"); 260 } 261 262 return input.success(existingSigningDetails); 263 } 264 } 265 266 /** 267 * Computes the minSdkVersion to use at runtime. If the package is not compatible with this 268 * platform, populates {@code outError[0]} with an error message. 269 * <p> 270 * If {@code minCode} is not specified, e.g. the value is {@code null}, then behavior varies 271 * based on the {@code platformSdkVersion}: 272 * <ul> 273 * <li>If the platform SDK version is greater than or equal to the 274 * {@code minVers}, returns the {@code mniVers} unmodified. 275 * <li>Otherwise, returns -1 to indicate that the package is not 276 * compatible with this platform. 277 * </ul> 278 * <p> 279 * Otherwise, the behavior varies based on whether the current platform 280 * is a pre-release version, e.g. the {@code platformSdkCodenames} array 281 * has length > 0: 282 * <ul> 283 * <li>If this is a pre-release platform and the value specified by 284 * {@code targetCode} is contained within the array of allowed pre-release 285 * codenames, this method will return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. 286 * <li>If this is a released platform, this method will return -1 to 287 * indicate that the package is not compatible with this platform. 288 * </ul> 289 * 290 * @param minVers minSdkVersion number, if specified in the application manifest, 291 * or 1 otherwise 292 * @param minCode minSdkVersion code, if specified in the application manifest, or 293 * {@code null} otherwise 294 * @param platformSdkVersion platform SDK version number, typically Build.VERSION.SDK_INT 295 * @param platformSdkCodenames array of allowed prerelease SDK codenames for this platform 296 * @return the minSdkVersion to use at runtime if successful 297 */ computeMinSdkVersion(@ntRangefrom = 1) int minVers, @Nullable String minCode, @IntRange(from = 1) int platformSdkVersion, @NonNull String[] platformSdkCodenames, @NonNull ParseInput input)298 public static ParseResult<Integer> computeMinSdkVersion(@IntRange(from = 1) int minVers, 299 @Nullable String minCode, @IntRange(from = 1) int platformSdkVersion, 300 @NonNull String[] platformSdkCodenames, @NonNull ParseInput input) { 301 // If it's a release SDK, make sure we meet the minimum SDK requirement. 302 if (minCode == null) { 303 if (minVers <= platformSdkVersion) { 304 return input.success(minVers); 305 } 306 307 // We don't meet the minimum SDK requirement. 308 return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, 309 "Requires newer sdk version #" + minVers 310 + " (current version is #" + platformSdkVersion + ")"); 311 } 312 313 // If it's a pre-release SDK and the codename matches this platform, we 314 // definitely meet the minimum SDK requirement. 315 if (matchTargetCode(platformSdkCodenames, minCode)) { 316 return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); 317 } 318 319 // Otherwise, we're looking at an incompatible pre-release SDK. 320 if (platformSdkCodenames.length > 0) { 321 return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, 322 "Requires development platform " + minCode 323 + " (current platform is any of " 324 + Arrays.toString(platformSdkCodenames) + ")"); 325 } else { 326 return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, 327 "Requires development platform " + minCode 328 + " but this is a release platform."); 329 } 330 } 331 332 /** 333 * Computes the targetSdkVersion to use at runtime. If the package is not compatible with this 334 * platform, populates {@code outError[0]} with an error message. 335 * <p> 336 * If {@code targetCode} is not specified, e.g. the value is {@code null}, then the {@code 337 * targetVers} will be returned unmodified. 338 * <p> 339 * When {@code allowUnknownCodenames} is false, the behavior varies based on whether the 340 * current platform is a pre-release version, e.g. the {@code platformSdkCodenames} array has 341 * length > 0: 342 * <ul> 343 * <li>If this is a pre-release platform and the value specified by 344 * {@code targetCode} is contained within the array of allowed pre-release 345 * codenames, this method will return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. 346 * <li>If this is a released platform, this method will return -1 to 347 * indicate that the package is not compatible with this platform. 348 * </ul> 349 * <p> 350 * When {@code allowUnknownCodenames} is true, any codename that is not known (presumed to be 351 * a codename announced after the build of the current device) is allowed and this method will 352 * return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. 353 * 354 * @param targetVers targetSdkVersion number, if specified in the application 355 * manifest, or 0 otherwise 356 * @param targetCode targetSdkVersion code, if specified in the application manifest, 357 * or {@code null} otherwise 358 * @param platformSdkCodenames array of allowed pre-release SDK codenames for this platform 359 * @param allowUnknownCodenames allow unknown codenames, if true this method will accept unknown 360 * (presumed to be future) codenames 361 * @return the targetSdkVersion to use at runtime if successful 362 */ computeTargetSdkVersion(@ntRangefrom = 0) int targetVers, @Nullable String targetCode, @NonNull String[] platformSdkCodenames, @NonNull ParseInput input, boolean allowUnknownCodenames)363 public static ParseResult<Integer> computeTargetSdkVersion(@IntRange(from = 0) int targetVers, 364 @Nullable String targetCode, @NonNull String[] platformSdkCodenames, 365 @NonNull ParseInput input, boolean allowUnknownCodenames) { 366 // If it's a release SDK, return the version number unmodified. 367 if (targetCode == null) { 368 return input.success(targetVers); 369 } 370 371 try { 372 if (allowUnknownCodenames && UnboundedSdkLevel.isAtMost(targetCode)) { 373 return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); 374 } 375 } catch (IllegalArgumentException e) { 376 // isAtMost() throws it when encountering an older SDK codename 377 return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, e.getMessage()); 378 } 379 380 // If it's a pre-release SDK and the codename matches this platform, it 381 // definitely targets this SDK. 382 if (matchTargetCode(platformSdkCodenames, targetCode)) { 383 return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); 384 } 385 386 // Otherwise, we're looking at an incompatible pre-release SDK. 387 if (platformSdkCodenames.length > 0) { 388 return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, 389 "Requires development platform " + targetCode 390 + " (current platform is any of " 391 + Arrays.toString(platformSdkCodenames) + ")"); 392 } else { 393 return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, 394 "Requires development platform " + targetCode 395 + " but this is a release platform."); 396 } 397 } 398 399 /** 400 * Computes the maxSdkVersion. If the package is not compatible with this platform, populates 401 * {@code outError[0]} with an error message. 402 * <p> 403 * {@code maxVers} is compared against {@code platformSdkVersion}. If {@code maxVers} is less 404 * than the {@code platformSdkVersion} then populates {@code outError[0]} with an error message. 405 * Otherwise, it returns {@code maxVers} unmodified. 406 * 407 * @param maxVers maxSdkVersion number, if specified in the application manifest, or {@code 408 * Integer.MAX_VALUE} otherwise 409 * @param platformSdkVersion platform SDK version number, typically Build.VERSION.SDK_INT 410 * @return the maxSdkVersion that was recognised or an error if the condition is not satisfied 411 */ computeMaxSdkVersion(@ntRangefrom = 0) int maxVers, @IntRange(from = 1) int platformSdkVersion, @NonNull ParseInput input)412 public static ParseResult<Integer> computeMaxSdkVersion(@IntRange(from = 0) int maxVers, 413 @IntRange(from = 1) int platformSdkVersion, @NonNull ParseInput input) { 414 if (platformSdkVersion > maxVers) { 415 return input.error(PackageManager.INSTALL_FAILED_NEWER_SDK, 416 "Requires max SDK version " + maxVers + " but is " 417 + platformSdkVersion); 418 } else { 419 return input.success(maxVers); 420 } 421 } 422 423 /** 424 * Matches a given {@code targetCode} against a set of release codeNames. Target codes can 425 * either be of the form {@code [codename]}" (e.g {@code "Q"}) or of the form {@code 426 * [codename].[fingerprint]} (e.g {@code "Q.cafebc561"}). 427 */ matchTargetCode(@onNull String[] codeNames, @NonNull String targetCode)428 private static boolean matchTargetCode(@NonNull String[] codeNames, 429 @NonNull String targetCode) { 430 final String targetCodeName; 431 final int targetCodeIdx = targetCode.indexOf('.'); 432 if (targetCodeIdx == -1) { 433 targetCodeName = targetCode; 434 } else { 435 targetCodeName = targetCode.substring(0, targetCodeIdx); 436 } 437 return ArrayUtils.contains(codeNames, targetCodeName); 438 } 439 } 440