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