Restrict updates of system packages

By declaring a <restrict-update> tag in its manifest, a system package
can restrict its update to be the singular package that has the same
given hash. An update's hash is the SHA-512 across all its APKs [i.e.
for splits, the SHA-512 is calculated over the concatenation of the
base plus all splits].

The restriction only applies to system packages.

Bug: 28398205
Change-Id: Iec493fc8ef27edee53f1d437cb0caaa78782f329
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 66c5000..6534f5b 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -159,6 +159,7 @@
     private static final String TAG_SUPPORTS_INPUT = "supports-input";
     private static final String TAG_EAT_COMMENT = "eat-comment";
     private static final String TAG_PACKAGE = "package";
+    private static final String TAG_RESTRICT_UPDATE = "restrict-update";
 
     // These are the tags supported by child packages
     private static final Set<String> CHILD_PACKAGE_TAGS = new ArraySet<>();
@@ -1639,9 +1640,9 @@
     /**
      * This is the common parsing routing for handling parent and child
      * packages in a base APK. The difference between parent and child
-     * parsing is that some targs are not supported by child packages as
+     * parsing is that some tags are not supported by child packages as
      * well as some manifest attributes are ignored. The implementation
-     * assumes the calling code already handled the manifest tag if needed
+     * assumes the calling code has already handled the manifest tag if needed
      * (this applies to the parent only).
      *
      * @param pkg The package which to populate
@@ -2089,6 +2090,29 @@
                     // If parsing a child failed the error is already set
                     return null;
                 }
+
+            } else if (tagName.equals(TAG_RESTRICT_UPDATE)) {
+                if ((flags & PARSE_IS_SYSTEM_DIR) != 0) {
+                    sa = res.obtainAttributes(parser,
+                            com.android.internal.R.styleable.AndroidManifestRestrictUpdate);
+                    final String hash = sa.getNonConfigurationString(
+                            com.android.internal.R.styleable.AndroidManifestRestrictUpdate_hash, 0);
+                    sa.recycle();
+
+                    pkg.restrictUpdateHash = null;
+                    if (hash != null) {
+                        final int hashLength = hash.length();
+                        final byte[] hashBytes = new byte[hashLength / 2];
+                        for (int i = 0; i < hashLength; i += 2){
+                            hashBytes[i/2] = (byte) ((Character.digit(hash.charAt(i), 16) << 4)
+                                    + Character.digit(hash.charAt(i + 1), 16));
+                        }
+                        pkg.restrictUpdateHash = hashBytes;
+                    }
+                }
+
+                XmlUtils.skipCurrentTag(parser);
+
             } else if (RIGID_PARSER) {
                 outError[0] = "Bad element under <manifest>: "
                     + parser.getName();
@@ -4822,6 +4846,8 @@
          */
         public boolean use32bitAbi;
 
+        public byte[] restrictUpdateHash;
+
         public Package(String packageName) {
             this.packageName = packageName;
             applicationInfo.packageName = packageName;
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 3fd75f7..191afe5 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2291,4 +2291,14 @@
         <attr name="minimalHeight" format="dimension" />
     </declare-styleable>
 
+    <!-- <code>restrict-update</code> tag restricts system apps from being updated unless the
+        SHA-512 hash equals the specified value.
+        @hide -->
+    <declare-styleable name="AndroidManifestRestrictUpdate" parent="AndroidManifest">
+        <!-- The SHA-512 hash of the only APK that can be used to update a package.
+             <p>NOTE: This is only applicable to system packages.
+             @hide -->
+        <attr name="hash" format="string" />
+    </declare-styleable>
+
 </resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 11df8e5..c91e09f 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2715,6 +2715,7 @@
     <public type="attr" name="contentInsetEndWithActions" />
     <public type="attr" name="numberPickerStyle" />
     <public type="attr" name="enableVrMode" />
+    <public type="attr" name="hash" />
 
     <public type="style" name="Theme.Material.Light.DialogWhenLarge.DarkActionBar" />
     <public type="style" name="Widget.Material.SeekBar.Discrete" />
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 4b0eeed..397e1ce 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -266,6 +266,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.FileReader;
@@ -274,6 +275,7 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.security.DigestInputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
@@ -13787,6 +13789,13 @@
         return false;
     }
 
+    private static void updateDigest(MessageDigest digest, File file) throws IOException {
+        try (DigestInputStream digestStream =
+                new DigestInputStream(new FileInputStream(file), digest)) {
+            while (digestStream.read() != -1) {} // nothing to do; just plow through the file
+        }
+    }
+
     private void replacePackageLIF(PackageParser.Package pkg, final int policyFlags, int scanFlags,
             UserHandle user, String installerPackageName, PackageInstalledInfo res) {
         final boolean isEphemeral = (policyFlags & PackageParser.PARSE_IS_EPHEMERAL) != 0;
@@ -13841,6 +13850,32 @@
                 }
             }
 
+            // don't allow a system upgrade unless the upgrade hash matches
+            if (oldPackage.restrictUpdateHash != null && oldPackage.isSystemApp()) {
+                byte[] digestBytes = null;
+                try {
+                    final MessageDigest digest = MessageDigest.getInstance("SHA-512");
+                    updateDigest(digest, new File(pkg.baseCodePath));
+                    if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
+                        for (String path : pkg.splitCodePaths) {
+                            updateDigest(digest, new File(path));
+                        }
+                    }
+                    digestBytes = digest.digest();
+                } catch (NoSuchAlgorithmException | IOException e) {
+                    res.setError(INSTALL_FAILED_INVALID_APK,
+                            "Could not compute hash: " + pkgName);
+                    return;
+                }
+                if (!Arrays.equals(oldPackage.restrictUpdateHash, digestBytes)) {
+                    res.setError(INSTALL_FAILED_INVALID_APK,
+                            "New package fails restrict-update check: " + pkgName);
+                    return;
+                }
+                // retain upgrade restriction
+                pkg.restrictUpdateHash = oldPackage.restrictUpdateHash;
+            }
+
             // Check for shared user id changes
             String invalidPackageName =
                     getParentOrChildPackageChangedSharedUser(oldPackage, pkg);