Manifest digest stored during package scanning

This allows things that scanned an APK to pass a hash of the manifest to
others that would then be able to compare it.

Change-Id: I465f5edf3acab2a952c3d321c8df2247ffe012ea
This commit is contained in:
Kenny Root
2011-08-08 16:19:08 -07:00
parent 4a51773e4a
commit bcc954d772
4 changed files with 233 additions and 7 deletions

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2011, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.pm;
parcelable ManifestDigest;

View File

@@ -0,0 +1,114 @@
package android.content.pm;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.util.Arrays;
import java.util.jar.Attributes;
/**
* Represents the manifest digest for a package. This is suitable for comparison
* of two packages to know whether the manifests are identical.
*
* @hide
*/
public class ManifestDigest implements Parcelable {
/** The digest of the manifest in our preferred order. */
private final byte[] mDigest;
/** Digest field names to look for in preferred order. */
private static final String[] DIGEST_TYPES = {
"SHA1-Digest", "SHA-Digest", "MD5-Digest",
};
/** What we print out first when toString() is called. */
private static final String TO_STRING_PREFIX = "ManifestDigest {mDigest=";
ManifestDigest(byte[] digest) {
mDigest = digest;
}
private ManifestDigest(Parcel source) {
mDigest = source.createByteArray();
}
static ManifestDigest fromAttributes(Attributes attributes) {
if (attributes == null) {
return null;
}
String encodedDigest = null;
for (int i = 0; i < DIGEST_TYPES.length; i++) {
final String value = attributes.getValue(DIGEST_TYPES[i]);
if (value != null) {
encodedDigest = value;
break;
}
}
if (encodedDigest == null) {
return null;
}
final byte[] digest = Base64.decode(encodedDigest, Base64.DEFAULT);
return new ManifestDigest(digest);
}
@Override
public int describeContents() {
return 0;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ManifestDigest)) {
return false;
}
final ManifestDigest other = (ManifestDigest) o;
return this == other || Arrays.equals(mDigest, other.mDigest);
}
@Override
public int hashCode() {
return Arrays.hashCode(mDigest);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(TO_STRING_PREFIX.length()
+ (mDigest.length * 3) + 1);
sb.append(TO_STRING_PREFIX);
final int N = mDigest.length;
for (int i = 0; i < N; i++) {
final byte b = mDigest[i];
IntegralToString.appendByteAsHex(sb, b, false);
sb.append(',');
}
sb.append('}');
return sb.toString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(mDigest);
}
public static final Parcelable.Creator<ManifestDigest> CREATOR
= new Parcelable.Creator<ManifestDigest>() {
public ManifestDigest createFromParcel(Parcel source) {
return new ManifestDigest(source);
}
public ManifestDigest[] newArray(int size) {
return new ManifestDigest[size];
}
};
}

View File

@@ -45,6 +45,7 @@ import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -59,6 +60,9 @@ public class PackageParser {
private static final boolean DEBUG_PARSER = false;
private static final boolean DEBUG_BACKUP = false;
/** File name in an APK for the Android manifest. */
private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
/** @hide */
public static class NewPermissionInfo {
public final String name;
@@ -402,7 +406,7 @@ public class PackageParser {
res = new Resources(assmgr, metrics, null);
assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Build.VERSION.RESOURCES_SDK_INT);
parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml");
parser = assmgr.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
assetError = false;
} else {
Slog.w(TAG, "Failed adding asset path:"+mArchiveSourcePath);
@@ -484,7 +488,7 @@ public class PackageParser {
// can trust it... we'll just use the AndroidManifest.xml
// to retrieve its signatures, not validating all of the
// files.
JarEntry jarEntry = jarFile.getJarEntry("AndroidManifest.xml");
JarEntry jarEntry = jarFile.getJarEntry(ANDROID_MANIFEST_FILENAME);
certs = loadCertificates(jarFile, jarEntry, readBuffer);
if (certs == null) {
Slog.e(TAG, "Package " + pkg.packageName
@@ -506,21 +510,30 @@ public class PackageParser {
}
}
}
} else {
Enumeration<JarEntry> entries = jarFile.entries();
final Manifest manifest = jarFile.getManifest();
while (entries.hasMoreElements()) {
final JarEntry je = entries.nextElement();
if (je.isDirectory()) continue;
if (je.getName().startsWith("META-INF/")) continue;
final Certificate[] localCerts = loadCertificates(jarFile, je,
readBuffer);
final String name = je.getName();
if (name.startsWith("META-INF/"))
continue;
if (ANDROID_MANIFEST_FILENAME.equals(name)) {
final Attributes attributes = manifest.getAttributes(name);
pkg.manifestDigest = ManifestDigest.fromAttributes(attributes);
}
final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);
if (DEBUG_JAR) {
Slog.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName()
+ ": certs=" + certs + " ("
+ (certs != null ? certs.length : 0) + ")");
}
if (localCerts == null) {
Slog.e(TAG, "Package " + pkg.packageName
+ " has no certificates at entry "
@@ -609,7 +622,7 @@ public class PackageParser {
return null;
}
parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml");
parser = assmgr.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
} catch (Exception e) {
if (assmgr != null) assmgr.close();
Slog.w(TAG, "Unable to read AndroidManifest.xml of "
@@ -2884,6 +2897,12 @@ public class PackageParser {
public int installLocation;
/**
* Digest suitable for comparing whether this package's manifest is the
* same as another.
*/
public ManifestDigest manifestDigest;
public Package(String _name) {
packageName = _name;
applicationInfo.packageName = _name;

View File

@@ -0,0 +1,74 @@
package android.content.pm;
import android.os.Parcel;
import android.test.AndroidTestCase;
import android.util.Base64;
import java.util.jar.Attributes;
public class ManifestDigestTest extends AndroidTestCase {
private static final byte[] DIGEST_1 = {
(byte) 0x00, (byte) 0xAA, (byte) 0x55, (byte) 0xFF
};
private static final String DIGEST_1_STR = Base64.encodeToString(DIGEST_1, Base64.DEFAULT);
private static final byte[] DIGEST_2 = {
(byte) 0x0A, (byte) 0xA5, (byte) 0xF0, (byte) 0x5A
};
private static final String DIGEST_2_STR = Base64.encodeToString(DIGEST_2, Base64.DEFAULT);
private static final Attributes.Name SHA1_DIGEST = new Attributes.Name("SHA1-Digest");
private static final Attributes.Name MD5_DIGEST = new Attributes.Name("MD5-Digest");
public void testManifestDigest_FromAttributes_Null() {
assertNull("Attributes were null, so ManifestDigest.fromAttributes should return null",
ManifestDigest.fromAttributes(null));
}
public void testManifestDigest_FromAttributes_NoAttributes() {
Attributes a = new Attributes();
assertNull("There were no attributes to extract, so ManifestDigest should be null",
ManifestDigest.fromAttributes(a));
}
public void testManifestDigest_FromAttributes_SHA1PreferredOverMD5() {
Attributes a = new Attributes();
a.put(SHA1_DIGEST, DIGEST_1_STR);
a.put(MD5_DIGEST, DIGEST_2_STR);
ManifestDigest fromAttributes = ManifestDigest.fromAttributes(a);
assertNotNull("A valid ManifestDigest should be returned", fromAttributes);
ManifestDigest created = new ManifestDigest(DIGEST_1);
assertEquals("SHA-1 should be preferred over MD5: " + created.toString() + " vs. "
+ fromAttributes.toString(), created, fromAttributes);
assertEquals("Hash codes should be the same: " + created.toString() + " vs. "
+ fromAttributes.toString(), created.hashCode(), fromAttributes
.hashCode());
}
public void testManifestDigest_Parcel() {
Attributes a = new Attributes();
a.put(SHA1_DIGEST, DIGEST_1_STR);
ManifestDigest digest = ManifestDigest.fromAttributes(a);
Parcel p = Parcel.obtain();
digest.writeToParcel(p, 0);
p.setDataPosition(0);
ManifestDigest fromParcel = ManifestDigest.CREATOR.createFromParcel(p);
assertEquals("ManifestDigest going through parceling should be the same as before: "
+ digest.toString() + " and " + fromParcel.toString(), digest,
fromParcel);
}
}