APK JAR signature verifier.
This adds JAR signature verification to ApkVerifier. Bug: 27461702 Change-Id: Id2b72bea7869be66268f6bc1387e1559ee02ff9d
This commit is contained in:
@@ -17,16 +17,24 @@
|
|||||||
package com.android.apksigner.core;
|
package com.android.apksigner.core;
|
||||||
|
|
||||||
import com.android.apksigner.core.apk.ApkUtils;
|
import com.android.apksigner.core.apk.ApkUtils;
|
||||||
|
import com.android.apksigner.core.internal.apk.v1.V1SchemeVerifier;
|
||||||
import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm;
|
import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm;
|
||||||
import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm;
|
import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm;
|
||||||
import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
|
import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
|
||||||
|
import com.android.apksigner.core.internal.util.AndroidSdkVersion;
|
||||||
import com.android.apksigner.core.util.DataSource;
|
import com.android.apksigner.core.util.DataSource;
|
||||||
import com.android.apksigner.core.zip.ZipFormatException;
|
import com.android.apksigner.core.zip.ZipFormatException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APK signature verifier which mimics the behavior of the Android platform.
|
* APK signature verifier which mimics the behavior of the Android platform.
|
||||||
@@ -36,6 +44,10 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class ApkVerifier {
|
public class ApkVerifier {
|
||||||
|
|
||||||
|
private static final int APK_SIGNATURE_SCHEME_V2_ID = 2;
|
||||||
|
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
|
||||||
|
Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the APK's signatures and returns the result of verification. The APK can be
|
* Verifies the APK's signatures and returns the result of verification. The APK can be
|
||||||
* considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
|
* considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
|
||||||
@@ -53,23 +65,96 @@ public class ApkVerifier {
|
|||||||
|
|
||||||
// Attempt to verify the APK using APK Signature Scheme v2
|
// Attempt to verify the APK using APK Signature Scheme v2
|
||||||
Result result = new Result();
|
Result result = new Result();
|
||||||
|
Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
|
||||||
try {
|
try {
|
||||||
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
|
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
|
||||||
|
foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID);
|
||||||
result.mergeFrom(v2Result);
|
result.mergeFrom(v2Result);
|
||||||
} catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
|
} catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
|
||||||
if (result.containsErrors()) {
|
if (result.containsErrors()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Verify JAR signature if necessary
|
// Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
|
||||||
if (!result.isVerifiedUsingV2Scheme()) {
|
// ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
|
||||||
|
// Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
|
||||||
|
// scheme) signatures were found.
|
||||||
|
if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) {
|
||||||
|
V1SchemeVerifier.Result v1Result =
|
||||||
|
V1SchemeVerifier.verify(
|
||||||
|
apk,
|
||||||
|
zipSections,
|
||||||
|
SUPPORTED_APK_SIG_SCHEME_NAMES,
|
||||||
|
foundApkSigSchemeIds,
|
||||||
|
minSdkVersion);
|
||||||
|
result.mergeFrom(v1Result);
|
||||||
|
}
|
||||||
|
if (result.containsErrors()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2
|
||||||
|
// signatures verified.
|
||||||
|
if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) {
|
||||||
|
ArrayList<Result.V1SchemeSignerInfo> v1Signers =
|
||||||
|
new ArrayList<>(result.getV1SchemeSigners());
|
||||||
|
ArrayList<Result.V2SchemeSignerInfo> v2Signers =
|
||||||
|
new ArrayList<>(result.getV2SchemeSigners());
|
||||||
|
ArrayList<ByteArray> v1SignerCerts = new ArrayList<>();
|
||||||
|
ArrayList<ByteArray> v2SignerCerts = new ArrayList<>();
|
||||||
|
for (Result.V1SchemeSignerInfo signer : v1Signers) {
|
||||||
|
try {
|
||||||
|
v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to encode JAR signer " + signer.getName() + " certs", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Result.V2SchemeSignerInfo signer : v2Signers) {
|
||||||
|
try {
|
||||||
|
v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to encode APK Signature Scheme v2 signer (index: "
|
||||||
|
+ signer.getIndex() + ") certs",
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < v1SignerCerts.size(); i++) {
|
||||||
|
ByteArray v1Cert = v1SignerCerts.get(i);
|
||||||
|
if (!v2SignerCerts.contains(v1Cert)) {
|
||||||
|
Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i);
|
||||||
|
v1Signer.addError(Issue.V2_SIG_MISSING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < v2SignerCerts.size(); i++) {
|
||||||
|
ByteArray v2Cert = v2SignerCerts.get(i);
|
||||||
|
if (!v1SignerCerts.contains(v2Cert)) {
|
||||||
|
Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i);
|
||||||
|
v2Signer.addError(Issue.JAR_SIG_MISSING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.containsErrors()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verified
|
// Verified
|
||||||
result.setVerified();
|
result.setVerified();
|
||||||
for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
|
if (result.isVerifiedUsingV2Scheme()) {
|
||||||
result.addSignerCertificate(signerInfo.getCertificate());
|
for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
|
||||||
|
result.addSignerCertificate(signerInfo.getCertificate());
|
||||||
|
}
|
||||||
|
} else if (result.isVerifiedUsingV1Scheme()) {
|
||||||
|
for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) {
|
||||||
|
result.addSignerCertificate(signerInfo.getCertificate());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"APK considered verified, but has not verified using either v1 or v2 schemes");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -83,9 +168,12 @@ public class ApkVerifier {
|
|||||||
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
||||||
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
||||||
private final List<X509Certificate> mSignerCerts = new ArrayList<>();
|
private final List<X509Certificate> mSignerCerts = new ArrayList<>();
|
||||||
|
private final List<V1SchemeSignerInfo> mV1SchemeSigners = new ArrayList<>();
|
||||||
|
private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
|
||||||
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
|
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
|
||||||
|
|
||||||
private boolean mVerified;
|
private boolean mVerified;
|
||||||
|
private boolean mVerifiedUsingV1Scheme;
|
||||||
private boolean mVerifiedUsingV2Scheme;
|
private boolean mVerifiedUsingV2Scheme;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,6 +187,13 @@ public class ApkVerifier {
|
|||||||
mVerified = true;
|
mVerified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the APK's JAR signatures verified.
|
||||||
|
*/
|
||||||
|
public boolean isVerifiedUsingV1Scheme() {
|
||||||
|
return mVerifiedUsingV1Scheme;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
|
* Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +212,27 @@ public class ApkVerifier {
|
|||||||
mSignerCerts.add(cert);
|
mSignerCerts.add(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns information about JAR signers associated with the APK's signature. These are the
|
||||||
|
* signers used by Android.
|
||||||
|
*
|
||||||
|
* @see #getV1SchemeIgnoredSigners()
|
||||||
|
*/
|
||||||
|
public List<V1SchemeSignerInfo> getV1SchemeSigners() {
|
||||||
|
return mV1SchemeSigners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns information about JAR signers ignored by the APK's signature verification
|
||||||
|
* process. These signers are ignored by Android. However, each signer's errors or warnings
|
||||||
|
* will contain information about why they are ignored.
|
||||||
|
*
|
||||||
|
* @see #getV1SchemeSigners()
|
||||||
|
*/
|
||||||
|
public List<V1SchemeSignerInfo> getV1SchemeIgnoredSigners() {
|
||||||
|
return mV1SchemeIgnoredSigners;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns information about APK Signature Scheme v2 signers associated with the APK's
|
* Returns information about APK Signature Scheme v2 signers associated with the APK's
|
||||||
* signature.
|
* signature.
|
||||||
@@ -139,6 +255,18 @@ public class ApkVerifier {
|
|||||||
return mWarnings;
|
return mWarnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void mergeFrom(V1SchemeVerifier.Result source) {
|
||||||
|
mVerifiedUsingV1Scheme = source.verified;
|
||||||
|
mErrors.addAll(source.getErrors());
|
||||||
|
mWarnings.addAll(source.getWarnings());
|
||||||
|
for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) {
|
||||||
|
mV1SchemeSigners.add(new V1SchemeSignerInfo(signer));
|
||||||
|
}
|
||||||
|
for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) {
|
||||||
|
mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void mergeFrom(V2SchemeVerifier.Result source) {
|
private void mergeFrom(V2SchemeVerifier.Result source) {
|
||||||
mVerifiedUsingV2Scheme = source.verified;
|
mVerifiedUsingV2Scheme = source.verified;
|
||||||
mErrors.addAll(source.getErrors());
|
mErrors.addAll(source.getErrors());
|
||||||
@@ -156,6 +284,13 @@ public class ApkVerifier {
|
|||||||
if (!mErrors.isEmpty()) {
|
if (!mErrors.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!mV1SchemeSigners.isEmpty()) {
|
||||||
|
for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
|
||||||
|
if (signer.containsErrors()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!mV2SchemeSigners.isEmpty()) {
|
if (!mV2SchemeSigners.isEmpty()) {
|
||||||
for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
|
for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
|
||||||
if (signer.containsErrors()) {
|
if (signer.containsErrors()) {
|
||||||
@@ -167,6 +302,98 @@ public class ApkVerifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a JAR signer associated with the APK's signature.
|
||||||
|
*/
|
||||||
|
public static class V1SchemeSignerInfo {
|
||||||
|
private final String mName;
|
||||||
|
private final List<X509Certificate> mCertChain;
|
||||||
|
private final String mSignatureBlockFileName;
|
||||||
|
private final String mSignatureFileName;
|
||||||
|
|
||||||
|
private final List<IssueWithParams> mErrors;
|
||||||
|
private final List<IssueWithParams> mWarnings;
|
||||||
|
|
||||||
|
private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) {
|
||||||
|
mName = result.name;
|
||||||
|
mCertChain = result.certChain;
|
||||||
|
mSignatureBlockFileName = result.signatureBlockFileName;
|
||||||
|
mSignatureFileName = result.signatureFileName;
|
||||||
|
mErrors = result.getErrors();
|
||||||
|
mWarnings = result.getWarnings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a user-friendly name of the signer.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the JAR entry containing this signer's JAR signature block file.
|
||||||
|
*/
|
||||||
|
public String getSignatureBlockFileName() {
|
||||||
|
return mSignatureBlockFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the JAR entry containing this signer's JAR signature file.
|
||||||
|
*/
|
||||||
|
public String getSignatureFileName() {
|
||||||
|
return mSignatureFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this signer's signing certificate or {@code null} if not available. The
|
||||||
|
* certificate is guaranteed to be available if no errors were encountered during
|
||||||
|
* verification (see {@link #containsErrors()}.
|
||||||
|
*
|
||||||
|
* <p>This certificate contains the signer's public key.
|
||||||
|
*/
|
||||||
|
public X509Certificate getCertificate() {
|
||||||
|
return mCertChain.isEmpty() ? null : mCertChain.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the certificate chain for the signer's public key. The certificate containing
|
||||||
|
* the public key is first, followed by the certificate (if any) which issued the
|
||||||
|
* signing certificate, and so forth. An empty list may be returned if an error was
|
||||||
|
* encountered during verification (see {@link #containsErrors()}).
|
||||||
|
*/
|
||||||
|
public List<X509Certificate> getCertificateChain() {
|
||||||
|
return mCertChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if an error was encountered while verifying this signer's JAR
|
||||||
|
* signature. Any error prevents the signer's signature from being considered verified.
|
||||||
|
*/
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return !mErrors.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns errors encountered while verifying this signer's JAR signature. Any error
|
||||||
|
* prevents the signer's signature from being considered verified.
|
||||||
|
*/
|
||||||
|
public List<IssueWithParams> getErrors() {
|
||||||
|
return mErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns warnings encountered while verifying this signer's JAR signature. Warnings
|
||||||
|
* do not prevent the signer's signature from being considered verified.
|
||||||
|
*/
|
||||||
|
public List<IssueWithParams> getWarnings() {
|
||||||
|
return mWarnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addError(Issue msg, Object... parameters) {
|
||||||
|
mErrors.add(new IssueWithParams(msg, parameters));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about an APK Signature Scheme v2 signer associated with the APK's signature.
|
* Information about an APK Signature Scheme v2 signer associated with the APK's signature.
|
||||||
*/
|
*/
|
||||||
@@ -212,6 +439,10 @@ public class ApkVerifier {
|
|||||||
return mCerts;
|
return mCerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addError(Issue msg, Object... parameters) {
|
||||||
|
mErrors.add(new IssueWithParams(msg, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean containsErrors() {
|
public boolean containsErrors() {
|
||||||
return !mErrors.isEmpty();
|
return !mErrors.isEmpty();
|
||||||
}
|
}
|
||||||
@@ -231,6 +462,324 @@ public class ApkVerifier {
|
|||||||
*/
|
*/
|
||||||
public static enum Issue {
|
public static enum Issue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK is not JAR-signed.
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_SIGNATURES("No JAR signatures"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK does not contain any entries covered by JAR signatures.
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK contains multiple entries with the same name.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest contains a section with a duplicate name.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: section name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest contains a section without a name.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: section index (1-based) ({@code Integer})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_UNNNAMED_MANIFEST_SECTION(
|
||||||
|
"Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature file contains a section without a name.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: signature file name ({@code String})</li>
|
||||||
|
* <li>Parameter 2: section index (1-based) ({@code Integer})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_UNNNAMED_SIG_FILE_SECTION(
|
||||||
|
"Malformed %1$s: invidual section #%2$d does not have a name"),
|
||||||
|
|
||||||
|
/** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */
|
||||||
|
JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest references an entry which is not there in the APK.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: entry name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST(
|
||||||
|
"%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest does not list a digest for the specified entry.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: entry name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature does not list a digest for the specified entry.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: entry name ({@code String})</li>
|
||||||
|
* <li>Parameter 2: signature file name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The specified JAR entry is not covered by JAR signature.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: entry name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature uses different set of signers to protect the two specified ZIP entries.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: first entry name ({@code String})</li>
|
||||||
|
* <li>Parameter 2: first entry signer names ({@code List<String>})</li>
|
||||||
|
* <li>Parameter 3: second entry name ({@code String})</li>
|
||||||
|
* <li>Parameter 4: second entry signer names ({@code List<String>})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH(
|
||||||
|
"Entries %1$s and %3$s are signed with different sets of signers"
|
||||||
|
+ " : <%2$s> vs <%4$s>"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest of the specified ZIP entry's data does not match the digest expected by the JAR
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: entry name ({@code String})</li>
|
||||||
|
* <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
|
||||||
|
* <li>Parameter 3: name of the entry in which the expected digest is specified
|
||||||
|
* ({@code String})</li>
|
||||||
|
* <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
|
||||||
|
* <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY(
|
||||||
|
"%2$s digest of %1$s does not match the digest specified in %3$s"
|
||||||
|
+ ". Expected: <%5$s>, actual: <%4$s>"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest of the JAR manifest main section did not verify.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})</li>
|
||||||
|
* <li>Parameter 2: name of the entry in which the expected digest is specified
|
||||||
|
* ({@code String})</li>
|
||||||
|
* <li>Parameter 3: base64-encoded actual digest ({@code String})</li>
|
||||||
|
* <li>Parameter 4: base64-encoded expected digest ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY(
|
||||||
|
"%1$s digest of META-INF/MANIFEST.MF main section does not match the digest"
|
||||||
|
+ " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest of the specified JAR manifest section does not match the digest expected by the
|
||||||
|
* JAR signature.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: section name ({@code String})</li>
|
||||||
|
* <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
|
||||||
|
* <li>Parameter 3: name of the signature file in which the expected digest is specified
|
||||||
|
* ({@code String})</li>
|
||||||
|
* <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
|
||||||
|
* <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY(
|
||||||
|
"%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest"
|
||||||
|
+ " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature file does not contain the whole-file digest of the JAR manifest file. The
|
||||||
|
* digest speeds up verification of JAR signature.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature file ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE(
|
||||||
|
"%1$s does not specify digest of META-INF/MANIFEST.MF"
|
||||||
|
+ ". This slows down verification."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not
|
||||||
|
* contain protections against stripping of these newer scheme signatures.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature file ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_APK_SIG_STRIP_PROTECTION(
|
||||||
|
"APK is signed using APK Signature Scheme v2 but these signatures may be stripped"
|
||||||
|
+ " without being detected because %1$s does not contain anti-stripping"
|
||||||
|
+ " protections."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature of the signer is missing a file/entry.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the encountered file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: name of the missing file ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception was encountered while verifying JAR signature contained in a signature block
|
||||||
|
* against the signature file.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature block file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: name of the signature file ({@code String})</li>
|
||||||
|
* <li>Parameter 3: exception ({@code Throwable})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception was encountered while parsing JAR signature contained in a signature block.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature block file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: exception ({@code Throwable})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception was encountered while parsing a certificate contained in the JAR signature
|
||||||
|
* block.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature block file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: exception ({@code Throwable})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature contained in a signature block file did not verify against the signature
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature block file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: name of the signature file ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature contains no verified signers.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature block file ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature file contains a section with a duplicate name.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: signature file name ({@code String})</li>
|
||||||
|
* <li>Parameter 1: section name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature file's main section doesn't contain the mandatory Signature-Version
|
||||||
|
* attribute.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: signature file name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE(
|
||||||
|
"Malformed %1$s: missing Signature-Version attribute"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature file references an unknown APK signature scheme ID.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID(
|
||||||
|
"JAR signature %1$s references unknown APK signature scheme ID: %2$d"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR signature file indicates that the APK is supposed to be signed with a supported APK
|
||||||
|
* signature scheme (in addition to the JAR signature) but no such signature was found in
|
||||||
|
* the APK.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: name of the signature file ({@code String})</li>
|
||||||
|
* <li>Parameter 2: APK signature scheme ID ({@code} Integer)</li>
|
||||||
|
* <li>Parameter 3: APK signature scheme English name ({@code} String)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_MISSING_APK_SIG_REFERENCED(
|
||||||
|
"JAR signature %1$s indicates the APK is signed using %3$s but no such signature"
|
||||||
|
+ " was found. Signature stripped?"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR entry is not covered by signature and thus unauthorized modifications to its contents
|
||||||
|
* will not be detected.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Parameter 1: entry name ({@code String})</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
JAR_SIG_UNPROTECTED_ZIP_ENTRY(
|
||||||
|
"%1$s not protected by signature. Unauthorized modifications to this JAR entry"
|
||||||
|
+ " will not be detected. Delete or move the entry outside of META-INF/."),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK
|
||||||
|
* Signature Scheme v2 signature from this signer, but does not contain a JAR signature
|
||||||
|
* from this signer.
|
||||||
|
*/
|
||||||
|
JAR_SIG_MISSING(
|
||||||
|
"No APK Signature Scheme v2 signature from this signer despite APK being v2"
|
||||||
|
+ " signed"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
|
||||||
|
* signature from this signer, but does not contain an APK Signature Scheme v2 signature
|
||||||
|
* from this signer.
|
||||||
|
*/
|
||||||
|
V2_SIG_MISSING(
|
||||||
|
"No APK Signature Scheme v2 signature from this signer despite APK being v2"
|
||||||
|
+ " signed"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
|
* Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
|
||||||
*/
|
*/
|
||||||
@@ -455,4 +1004,42 @@ public class ApkVerifier {
|
|||||||
return String.format(mIssue.getFormat(), mParams);
|
return String.format(mIssue.getFormat(), mParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate
|
||||||
|
* on the contents of the arrays rather than on references.
|
||||||
|
*/
|
||||||
|
private static class ByteArray {
|
||||||
|
private final byte[] mArray;
|
||||||
|
|
||||||
|
private ByteArray(byte[] arr) {
|
||||||
|
mArray = arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + Arrays.hashCode(mArray);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ByteArray other = (ByteArray) obj;
|
||||||
|
if (!Arrays.equals(mArray, other.mArray)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -79,8 +79,9 @@ public abstract class V1SchemeSigner {
|
|||||||
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
||||||
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
||||||
|
|
||||||
|
static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
|
||||||
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
||||||
new Attributes.Name("X-Android-APK-Signed");
|
new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signer configuration.
|
* Signer configuration.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,339 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 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 com.android.apksigner.core.internal.jar;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest and signature file parser.
|
||||||
|
*
|
||||||
|
* <p>These files consist of a main section followed by individual sections. Individual sections
|
||||||
|
* are named, their names referring to JAR entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public class ManifestParser {
|
||||||
|
|
||||||
|
private final byte[] mManifest;
|
||||||
|
private int mOffset;
|
||||||
|
private int mEndOffset;
|
||||||
|
|
||||||
|
private String mBufferedLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ManifestParser} with the provided input.
|
||||||
|
*/
|
||||||
|
public ManifestParser(byte[] data) {
|
||||||
|
this(data, 0, data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ManifestParser} with the provided input.
|
||||||
|
*/
|
||||||
|
public ManifestParser(byte[] data, int offset, int length) {
|
||||||
|
mManifest = data;
|
||||||
|
mOffset = offset;
|
||||||
|
mEndOffset = offset + length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remaining sections of this file.
|
||||||
|
*/
|
||||||
|
public List<Section> readAllSections() {
|
||||||
|
List<Section> sections = new ArrayList<>();
|
||||||
|
Section section;
|
||||||
|
while ((section = readSection()) != null) {
|
||||||
|
sections.add(section);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next section from this file or {@code null} if end of file has been reached.
|
||||||
|
*/
|
||||||
|
public Section readSection() {
|
||||||
|
// Locate the first non-empty line
|
||||||
|
int sectionStartOffset;
|
||||||
|
String attr;
|
||||||
|
do {
|
||||||
|
sectionStartOffset = mOffset;
|
||||||
|
attr = readAttribute();
|
||||||
|
if (attr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} while (attr.length() == 0);
|
||||||
|
List<Attribute> attrs = new ArrayList<>();
|
||||||
|
attrs.add(parseAttr(attr));
|
||||||
|
|
||||||
|
// Read attributes until end of section reached
|
||||||
|
while (true) {
|
||||||
|
attr = readAttribute();
|
||||||
|
if ((attr == null) || (attr.length() == 0)) {
|
||||||
|
// End of section
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attrs.add(parseAttr(attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sectionEndOffset = mOffset;
|
||||||
|
int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
|
||||||
|
|
||||||
|
return new Section(sectionStartOffset, sectionSizeBytes, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Attribute parseAttr(String attr) {
|
||||||
|
int delimiterIndex = attr.indexOf(':');
|
||||||
|
if (delimiterIndex == -1) {
|
||||||
|
return new Attribute(attr.trim(), "");
|
||||||
|
} else {
|
||||||
|
return new Attribute(
|
||||||
|
attr.substring(0, delimiterIndex).trim(),
|
||||||
|
attr.substring(delimiterIndex + 1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next attribute or empty {@code String} if end of section has been reached or
|
||||||
|
* {@code null} if end of input has been reached.
|
||||||
|
*/
|
||||||
|
private String readAttribute() {
|
||||||
|
// Check whether end of section was reached during previous invocation
|
||||||
|
if ((mBufferedLine != null) && (mBufferedLine.length() == 0)) {
|
||||||
|
mBufferedLine = null;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the next line
|
||||||
|
String line = readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// End of input
|
||||||
|
if (mBufferedLine != null) {
|
||||||
|
String result = mBufferedLine;
|
||||||
|
mBufferedLine = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the read line
|
||||||
|
if (line.length() == 0) {
|
||||||
|
// End of section
|
||||||
|
if (mBufferedLine != null) {
|
||||||
|
String result = mBufferedLine;
|
||||||
|
mBufferedLine = "";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder attrLine;
|
||||||
|
if (mBufferedLine == null) {
|
||||||
|
attrLine = new StringBuilder(line);
|
||||||
|
} else {
|
||||||
|
if (!line.startsWith(" ")) {
|
||||||
|
// The most common case: buffered line is a full attribute
|
||||||
|
String result = mBufferedLine;
|
||||||
|
mBufferedLine = line;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
attrLine = new StringBuilder(mBufferedLine);
|
||||||
|
mBufferedLine = null;
|
||||||
|
attrLine.append(line.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything's buffered in attrLine now. mBufferedLine is null
|
||||||
|
|
||||||
|
// Read more lines
|
||||||
|
while (true) {
|
||||||
|
line = readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// End of input
|
||||||
|
return attrLine.toString();
|
||||||
|
} else if (line.length() == 0) {
|
||||||
|
// End of section
|
||||||
|
mBufferedLine = ""; // make this method return "end of section" next time
|
||||||
|
return attrLine.toString();
|
||||||
|
}
|
||||||
|
if (line.startsWith(" ")) {
|
||||||
|
// Continuation line
|
||||||
|
attrLine.append(line.substring(1));
|
||||||
|
} else {
|
||||||
|
// Next attribute
|
||||||
|
mBufferedLine = line;
|
||||||
|
return attrLine.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next line (without line delimiter characters) or {@code null} if end of input has
|
||||||
|
* been reached.
|
||||||
|
*/
|
||||||
|
private String readLine() {
|
||||||
|
if (mOffset >= mEndOffset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int startOffset = mOffset;
|
||||||
|
int newlineStartOffset = -1;
|
||||||
|
int newlineEndOffset = -1;
|
||||||
|
for (int i = startOffset; i < mEndOffset; i++) {
|
||||||
|
byte b = mManifest[i];
|
||||||
|
if (b == '\r') {
|
||||||
|
newlineStartOffset = i;
|
||||||
|
int nextIndex = i + 1;
|
||||||
|
if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
|
||||||
|
newlineEndOffset = nextIndex + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newlineEndOffset = nextIndex;
|
||||||
|
break;
|
||||||
|
} else if (b == '\n') {
|
||||||
|
newlineStartOffset = i;
|
||||||
|
newlineEndOffset = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newlineStartOffset == -1) {
|
||||||
|
newlineStartOffset = mEndOffset;
|
||||||
|
newlineEndOffset = mEndOffset;
|
||||||
|
}
|
||||||
|
mOffset = newlineEndOffset;
|
||||||
|
|
||||||
|
int lineLengthBytes = newlineStartOffset - startOffset;
|
||||||
|
if (lineLengthBytes == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(mManifest, startOffset, lineLengthBytes, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute.
|
||||||
|
*/
|
||||||
|
public static class Attribute {
|
||||||
|
private final String mName;
|
||||||
|
private final String mValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Attribute} with the provided name and value.
|
||||||
|
*/
|
||||||
|
public Attribute(String name, String value) {
|
||||||
|
mName = name;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this attribute's name.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this attribute's value.
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section.
|
||||||
|
*/
|
||||||
|
public static class Section {
|
||||||
|
private final int mStartOffset;
|
||||||
|
private final int mSizeBytes;
|
||||||
|
private final String mName;
|
||||||
|
private final List<Attribute> mAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Section}.
|
||||||
|
*
|
||||||
|
* @param startOffset start offset (in bytes) of the section in the input file
|
||||||
|
* @param sizeBytes size (in bytes) of the section in the input file
|
||||||
|
* @param attrs attributes contained in the section
|
||||||
|
*/
|
||||||
|
public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
|
||||||
|
mStartOffset = startOffset;
|
||||||
|
mSizeBytes = sizeBytes;
|
||||||
|
String sectionName = null;
|
||||||
|
if (!attrs.isEmpty()) {
|
||||||
|
Attribute firstAttr = attrs.get(0);
|
||||||
|
if ("Name".equalsIgnoreCase(firstAttr.getName())) {
|
||||||
|
sectionName = firstAttr.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mName = sectionName;
|
||||||
|
mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the offset (in bytes) at which this section starts in the input.
|
||||||
|
*/
|
||||||
|
public int getStartOffset() {
|
||||||
|
return mStartOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) of this section in the input.
|
||||||
|
*/
|
||||||
|
public int getSizeBytes() {
|
||||||
|
return mSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this section's attributes, in the order in which they appear in the input.
|
||||||
|
*/
|
||||||
|
public List<Attribute> getAttributes() {
|
||||||
|
return mAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the specified attribute in this section or {@code null} if this
|
||||||
|
* section does not contain a matching attribute.
|
||||||
|
*/
|
||||||
|
public String getAttributeValue(Attributes.Name name) {
|
||||||
|
return getAttributeValue(name.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the specified attribute in this section or {@code null} if this
|
||||||
|
* section does not contain a matching attribute.
|
||||||
|
*
|
||||||
|
* @param name name of the attribute. Attribute names are case-insensitive.
|
||||||
|
*/
|
||||||
|
public String getAttributeValue(String name) {
|
||||||
|
for (Attribute attr : mAttributes) {
|
||||||
|
if (attr.getName().equalsIgnoreCase(name)) {
|
||||||
|
return attr.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -26,6 +26,8 @@ import java.util.jar.Attributes;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Producer of {@code META-INF/MANIFEST.MF} file.
|
* Producer of {@code META-INF/MANIFEST.MF} file.
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
*/
|
*/
|
||||||
public abstract class ManifestWriter {
|
public abstract class ManifestWriter {
|
||||||
|
|
||||||
|
@@ -23,6 +23,8 @@ import java.util.jar.Attributes;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Producer of JAR signature file ({@code *.SF}).
|
* Producer of JAR signature file ({@code *.SF}).
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
*/
|
*/
|
||||||
public abstract class SignatureFileWriter {
|
public abstract class SignatureFileWriter {
|
||||||
private SignatureFileWriter() {}
|
private SignatureFileWriter() {}
|
||||||
|
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 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 com.android.apksigner.core.internal.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android SDK version / API Level constants.
|
||||||
|
*/
|
||||||
|
public abstract class AndroidSdkVersion {
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private AndroidSdkVersion() {}
|
||||||
|
|
||||||
|
/** Android 4.3. The revenge of the beans. */
|
||||||
|
public static final int JELLY_BEAN_MR2 = 18;
|
||||||
|
|
||||||
|
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
|
||||||
|
public static final int LOLLIPOP = 21;
|
||||||
|
|
||||||
|
// TODO: Update Javadoc / constant name once N is assigned a proper name / version code.
|
||||||
|
/** Android N. */
|
||||||
|
public static final int N = 24;
|
||||||
|
}
|
@@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 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 com.android.apksigner.core.internal.zip;
|
||||||
|
|
||||||
|
import com.android.apksigner.core.zip.ZipFormatException;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZIP Central Directory (CD) Record.
|
||||||
|
*/
|
||||||
|
public class CentralDirectoryRecord {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparator which compares records by the offset of the corresponding Local File Header in the
|
||||||
|
* archive.
|
||||||
|
*/
|
||||||
|
public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR =
|
||||||
|
new ByLocalFileHeaderOffsetComparator();
|
||||||
|
|
||||||
|
private static final int RECORD_SIGNATURE = 0x02014b50;
|
||||||
|
private static final int HEADER_SIZE_BYTES = 46;
|
||||||
|
|
||||||
|
private static final int GP_FLAGS_OFFSET = 8;
|
||||||
|
private static final int COMPRESSION_METHOD_OFFSET = 10;
|
||||||
|
private static final int CRC32_OFFSET = 16;
|
||||||
|
private static final int COMPRESSED_SIZE_OFFSET = 20;
|
||||||
|
private static final int UNCOMPRESSED_SIZE_OFFSET = 24;
|
||||||
|
private static final int NAME_LENGTH_OFFSET = 28;
|
||||||
|
private static final int EXTRA_LENGTH_OFFSET = 30;
|
||||||
|
private static final int COMMENT_LENGTH_OFFSET = 32;
|
||||||
|
private static final int LOCAL_FILE_HEADER_OFFSET = 42;
|
||||||
|
private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
|
||||||
|
|
||||||
|
private final short mGpFlags;
|
||||||
|
private final short mCompressionMethod;
|
||||||
|
private final long mCrc32;
|
||||||
|
private final long mCompressedSize;
|
||||||
|
private final long mUncompressedSize;
|
||||||
|
private final long mLocalFileHeaderOffset;
|
||||||
|
private final String mName;
|
||||||
|
|
||||||
|
private CentralDirectoryRecord(
|
||||||
|
short gpFlags,
|
||||||
|
short compressionMethod,
|
||||||
|
long crc32,
|
||||||
|
long compressedSize,
|
||||||
|
long uncompressedSize,
|
||||||
|
long localFileHeaderOffset,
|
||||||
|
String name) {
|
||||||
|
mGpFlags = gpFlags;
|
||||||
|
mCompressionMethod = compressionMethod;
|
||||||
|
mCrc32 = crc32;
|
||||||
|
mCompressedSize = compressedSize;
|
||||||
|
mUncompressedSize = uncompressedSize;
|
||||||
|
mLocalFileHeaderOffset = localFileHeaderOffset;
|
||||||
|
mName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getGpFlags() {
|
||||||
|
return mGpFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getCompressionMethod() {
|
||||||
|
return mCompressionMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCrc32() {
|
||||||
|
return mCrc32;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCompressedSize() {
|
||||||
|
return mCompressedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUncompressedSize() {
|
||||||
|
return mUncompressedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLocalFileHeaderOffset() {
|
||||||
|
return mLocalFileHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Central Directory Record starting at the current position of the provided buffer
|
||||||
|
* and advances the buffer's position immediately past the end of the record.
|
||||||
|
*/
|
||||||
|
public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException {
|
||||||
|
ZipUtils.assertByteOrderLittleEndian(buf);
|
||||||
|
if (buf.remaining() < HEADER_SIZE_BYTES) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Input too short. Need at least: " + HEADER_SIZE_BYTES
|
||||||
|
+ " bytes, available: " + buf.remaining() + " bytes",
|
||||||
|
new BufferUnderflowException());
|
||||||
|
}
|
||||||
|
int bufPosition = buf.position();
|
||||||
|
int recordSignature = buf.getInt(bufPosition);
|
||||||
|
if (recordSignature != RECORD_SIGNATURE) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Not a Central Directory record. Signature: 0x"
|
||||||
|
+ Long.toHexString(recordSignature & 0xffffffffL));
|
||||||
|
}
|
||||||
|
short gpFlags = buf.getShort(bufPosition + GP_FLAGS_OFFSET);
|
||||||
|
short compressionMethod = buf.getShort(bufPosition + COMPRESSION_METHOD_OFFSET);
|
||||||
|
long crc32 = ZipUtils.getUnsignedInt32(buf, bufPosition + CRC32_OFFSET);
|
||||||
|
long compressedSize = ZipUtils.getUnsignedInt32(buf, bufPosition + COMPRESSED_SIZE_OFFSET);
|
||||||
|
long uncompressedSize =
|
||||||
|
ZipUtils.getUnsignedInt32(buf, bufPosition + UNCOMPRESSED_SIZE_OFFSET);
|
||||||
|
int nameSize = ZipUtils.getUnsignedInt16(buf, bufPosition + NAME_LENGTH_OFFSET);
|
||||||
|
int extraSize = ZipUtils.getUnsignedInt16(buf, bufPosition + EXTRA_LENGTH_OFFSET);
|
||||||
|
int commentSize = ZipUtils.getUnsignedInt16(buf, bufPosition + COMMENT_LENGTH_OFFSET);
|
||||||
|
long localFileHeaderOffset =
|
||||||
|
ZipUtils.getUnsignedInt32(buf, bufPosition + LOCAL_FILE_HEADER_OFFSET);
|
||||||
|
int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
|
||||||
|
if (recordSize > buf.remaining()) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Input too short. Need: " + recordSize + " bytes, available: "
|
||||||
|
+ buf.remaining() + " bytes",
|
||||||
|
new BufferUnderflowException());
|
||||||
|
}
|
||||||
|
String name = getName(buf, bufPosition + NAME_OFFSET, nameSize);
|
||||||
|
buf.position(bufPosition + recordSize);
|
||||||
|
return new CentralDirectoryRecord(
|
||||||
|
gpFlags,
|
||||||
|
compressionMethod,
|
||||||
|
crc32,
|
||||||
|
compressedSize,
|
||||||
|
uncompressedSize,
|
||||||
|
localFileHeaderOffset,
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getName(ByteBuffer record, int position, int nameLengthBytes) {
|
||||||
|
byte[] nameBytes;
|
||||||
|
int nameBytesOffset;
|
||||||
|
if (record.hasArray()) {
|
||||||
|
nameBytes = record.array();
|
||||||
|
nameBytesOffset = record.arrayOffset() + position;
|
||||||
|
} else {
|
||||||
|
nameBytes = new byte[nameLengthBytes];
|
||||||
|
nameBytesOffset = 0;
|
||||||
|
int originalPosition = record.position();
|
||||||
|
try {
|
||||||
|
record.position(position);
|
||||||
|
record.get(nameBytes);
|
||||||
|
} finally {
|
||||||
|
record.position(originalPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(nameBytes, nameBytesOffset, nameLengthBytes, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByLocalFileHeaderOffsetComparator
|
||||||
|
implements Comparator<CentralDirectoryRecord> {
|
||||||
|
@Override
|
||||||
|
public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) {
|
||||||
|
long offset1 = r1.getLocalFileHeaderOffset();
|
||||||
|
long offset2 = r2.getLocalFileHeaderOffset();
|
||||||
|
if (offset1 > offset2) {
|
||||||
|
return 1;
|
||||||
|
} else if (offset1 < offset2) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,281 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 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 com.android.apksigner.core.internal.zip;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.zip.DataFormatException;
|
||||||
|
import java.util.zip.Inflater;
|
||||||
|
|
||||||
|
import com.android.apksigner.core.internal.util.ByteBufferSink;
|
||||||
|
import com.android.apksigner.core.util.DataSink;
|
||||||
|
import com.android.apksigner.core.util.DataSource;
|
||||||
|
import com.android.apksigner.core.zip.ZipFormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZIP Local File Header.
|
||||||
|
*/
|
||||||
|
public class LocalFileHeader {
|
||||||
|
private static final int RECORD_SIGNATURE = 0x04034b50;
|
||||||
|
private static final int HEADER_SIZE_BYTES = 30;
|
||||||
|
|
||||||
|
private static final int GP_FLAGS_OFFSET = 6;
|
||||||
|
private static final int COMPRESSION_METHOD_OFFSET = 8;
|
||||||
|
private static final int CRC32_OFFSET = 14;
|
||||||
|
private static final int COMPRESSED_SIZE_OFFSET = 18;
|
||||||
|
private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
|
||||||
|
private static final int NAME_LENGTH_OFFSET = 26;
|
||||||
|
private static final int EXTRA_LENGTH_OFFSET = 28;
|
||||||
|
private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
|
||||||
|
|
||||||
|
private static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
|
||||||
|
|
||||||
|
private LocalFileHeader() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
|
||||||
|
*/
|
||||||
|
public static byte[] getUncompressedData(
|
||||||
|
DataSource source,
|
||||||
|
long sourceOffsetInArchive,
|
||||||
|
CentralDirectoryRecord cdRecord,
|
||||||
|
long cdStartOffsetInArchive) throws ZipFormatException, IOException {
|
||||||
|
if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
|
||||||
|
throw new IOException(
|
||||||
|
cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
|
||||||
|
}
|
||||||
|
byte[] result = new byte[(int) cdRecord.getUncompressedSize()];
|
||||||
|
ByteBuffer resultBuf = ByteBuffer.wrap(result);
|
||||||
|
ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
|
||||||
|
sendUncompressedData(
|
||||||
|
source,
|
||||||
|
sourceOffsetInArchive,
|
||||||
|
cdRecord,
|
||||||
|
cdStartOffsetInArchive,
|
||||||
|
resultSink);
|
||||||
|
if (resultBuf.hasRemaining()) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Data of " + cdRecord.getName() + " shorter than specified in Central Directory"
|
||||||
|
+ ". Expected: " + result.length + " bytes, read: "
|
||||||
|
+ resultBuf.position() + " bytes");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the uncompressed data pointed to by the provided ZIP Central Directory (CD) record into
|
||||||
|
* the provided data sink.
|
||||||
|
*/
|
||||||
|
public static void sendUncompressedData(
|
||||||
|
DataSource source,
|
||||||
|
long sourceOffsetInArchive,
|
||||||
|
CentralDirectoryRecord cdRecord,
|
||||||
|
long cdStartOffsetInArchive,
|
||||||
|
DataSink sink) throws ZipFormatException, IOException {
|
||||||
|
|
||||||
|
// IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
|
||||||
|
// exhibited when reading an APK for the purposes of verifying its signatures.
|
||||||
|
|
||||||
|
String entryName = cdRecord.getName();
|
||||||
|
byte[] cdNameBytes = entryName.getBytes("UTF-8");
|
||||||
|
int headerSizeWithName = HEADER_SIZE_BYTES + cdNameBytes.length;
|
||||||
|
long localFileHeaderOffsetInArchive = cdRecord.getLocalFileHeaderOffset();
|
||||||
|
long headerEndInArchive = localFileHeaderOffsetInArchive + headerSizeWithName;
|
||||||
|
if (headerEndInArchive >= cdStartOffsetInArchive) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Local File Header of " + entryName + " extends beyond start of Central"
|
||||||
|
+ " Directory. LFH end: " + headerEndInArchive
|
||||||
|
+ ", CD start: " + cdStartOffsetInArchive);
|
||||||
|
}
|
||||||
|
ByteBuffer header;
|
||||||
|
try {
|
||||||
|
header =
|
||||||
|
source.getByteBuffer(
|
||||||
|
localFileHeaderOffsetInArchive - sourceOffsetInArchive,
|
||||||
|
headerSizeWithName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException("Failed to read Local File Header of " + entryName, e);
|
||||||
|
}
|
||||||
|
header.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
int recordSignature = header.getInt(0);
|
||||||
|
if (recordSignature != RECORD_SIGNATURE) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Not a Local File Header record for entry " + entryName + ". Signature: 0x"
|
||||||
|
+ Long.toHexString(recordSignature & 0xffffffffL));
|
||||||
|
}
|
||||||
|
short gpFlags = header.getShort(GP_FLAGS_OFFSET);
|
||||||
|
if ((gpFlags & GP_FLAG_DATA_DESCRIPTOR_USED) == 0) {
|
||||||
|
long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
|
||||||
|
if (crc32 != cdRecord.getCrc32()) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"CRC-32 mismatch between Local File Header and Central Directory for entry "
|
||||||
|
+ entryName + ". LFH: " + crc32 + ", CD: " + cdRecord.getCrc32());
|
||||||
|
}
|
||||||
|
long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
|
||||||
|
if (compressedSize != cdRecord.getCompressedSize()) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Compressed size mismatch between Local File Header and Central Directory"
|
||||||
|
+ " for entry " + entryName + ". LFH: " + compressedSize
|
||||||
|
+ ", CD: " + cdRecord.getCompressedSize());
|
||||||
|
}
|
||||||
|
long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
|
||||||
|
if (uncompressedSize != cdRecord.getUncompressedSize()) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Uncompressed size mismatch between Local File Header and Central Directory"
|
||||||
|
+ " for entry " + entryName + ". LFH: " + uncompressedSize
|
||||||
|
+ ", CD: " + cdRecord.getUncompressedSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
|
||||||
|
if (nameLength > cdNameBytes.length) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Name mismatch between Local File Header and Central Directory for entry"
|
||||||
|
+ entryName + ". LFH: " + nameLength
|
||||||
|
+ " bytes, CD: " + cdNameBytes.length + " bytes");
|
||||||
|
}
|
||||||
|
String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
|
||||||
|
if (!entryName.equals(name)) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Name mismatch between Local File Header and Central Directory. LFH: \""
|
||||||
|
+ name + "\", CD: \"" + entryName + "\"");
|
||||||
|
}
|
||||||
|
int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
|
||||||
|
|
||||||
|
short compressionMethod = header.getShort(COMPRESSION_METHOD_OFFSET);
|
||||||
|
boolean compressed;
|
||||||
|
switch (compressionMethod) {
|
||||||
|
case ZipUtils.COMPRESSION_METHOD_STORED:
|
||||||
|
compressed = false;
|
||||||
|
break;
|
||||||
|
case ZipUtils.COMPRESSION_METHOD_DEFLATED:
|
||||||
|
compressed = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Unsupported compression method of entry " + entryName
|
||||||
|
+ ": " + (compressionMethod & 0xffff));
|
||||||
|
}
|
||||||
|
|
||||||
|
long dataStartOffsetInArchive =
|
||||||
|
localFileHeaderOffsetInArchive + HEADER_SIZE_BYTES + nameLength + extraLength;
|
||||||
|
long dataSize;
|
||||||
|
if (compressed) {
|
||||||
|
dataSize = cdRecord.getCompressedSize();
|
||||||
|
} else {
|
||||||
|
dataSize = cdRecord.getUncompressedSize();
|
||||||
|
}
|
||||||
|
long dataEndOffsetInArchive = dataStartOffsetInArchive + dataSize;
|
||||||
|
if (dataEndOffsetInArchive > cdStartOffsetInArchive) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"Local File Header data of " + entryName + " extends beyond Central Directory"
|
||||||
|
+ ". LFH data start: " + dataStartOffsetInArchive
|
||||||
|
+ ", LFH data end: " + dataEndOffsetInArchive
|
||||||
|
+ ", CD start: " + cdStartOffsetInArchive);
|
||||||
|
}
|
||||||
|
|
||||||
|
long dataOffsetInSource = dataStartOffsetInArchive - sourceOffsetInArchive;
|
||||||
|
try {
|
||||||
|
if (compressed) {
|
||||||
|
try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
|
||||||
|
source.feed(dataOffsetInSource, dataSize, inflateAdapter);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source.feed(dataOffsetInSource, dataSize, sink);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Failed to read data of " + ((compressed) ? "compressed" : "uncompressed")
|
||||||
|
+ " entry " + entryName,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
// Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
|
||||||
|
// thus don't check either.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InflateSinkAdapter implements DataSink, Closeable {
|
||||||
|
private final DataSink mDelegate;
|
||||||
|
|
||||||
|
private Inflater mInflater = new Inflater(true);
|
||||||
|
private byte[] mOutputBuffer;
|
||||||
|
private byte[] mInputBuffer;
|
||||||
|
private boolean mClosed;
|
||||||
|
|
||||||
|
private InflateSinkAdapter(DataSink delegate) {
|
||||||
|
mDelegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
mInflater.setInput(buf, offset, length);
|
||||||
|
if (mOutputBuffer == null) {
|
||||||
|
mOutputBuffer = new byte[65536];
|
||||||
|
}
|
||||||
|
while (!mInflater.finished()) {
|
||||||
|
int outputChunkSize;
|
||||||
|
try {
|
||||||
|
outputChunkSize = mInflater.inflate(mOutputBuffer);
|
||||||
|
} catch (DataFormatException e) {
|
||||||
|
throw new IOException("Failed to inflate data", e);
|
||||||
|
}
|
||||||
|
if (outputChunkSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
|
||||||
|
mDelegate.consume(ByteBuffer.wrap(mOutputBuffer, 0, outputChunkSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
if (buf.hasArray()) {
|
||||||
|
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
|
||||||
|
buf.position(buf.limit());
|
||||||
|
} else {
|
||||||
|
if (mInputBuffer == null) {
|
||||||
|
mInputBuffer = new byte[65536];
|
||||||
|
}
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
|
||||||
|
buf.get(mInputBuffer, 0, chunkSize);
|
||||||
|
consume(mInputBuffer, 0, chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
mClosed = true;
|
||||||
|
mInputBuffer = null;
|
||||||
|
mOutputBuffer = null;
|
||||||
|
if (mInflater != null) {
|
||||||
|
mInflater.end();
|
||||||
|
mInflater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotClosed() {
|
||||||
|
if (mClosed) {
|
||||||
|
throw new IllegalStateException("Closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -255,13 +255,13 @@ public abstract class ZipUtils {
|
|||||||
return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
|
return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
|
static void assertByteOrderLittleEndian(ByteBuffer buffer) {
|
||||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
|
public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
|
||||||
return buffer.getShort(offset) & 0xffff;
|
return buffer.getShort(offset) & 0xffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ public abstract class ZipUtils {
|
|||||||
buffer.putInt(offset, (int) value);
|
buffer.putInt(offset, (int) value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
static long getUnsignedInt32(ByteBuffer buffer, int offset) {
|
||||||
return buffer.getInt(offset) & 0xffffffffL;
|
return buffer.getInt(offset) & 0xffffffffL;
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user