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);