Merge "APK Signature Scheme v2 APK verifier."
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.android.apksigner.core.apk.ApkUtils;
|
||||
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.V2SchemeVerifier;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
import com.android.apksigner.core.zip.ZipFormatException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* APK signature verifier which mimics the behavior of the Android platform.
|
||||
*
|
||||
* <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
|
||||
* the verifier to be used for checking whether an APK's signatures will verify on Android.
|
||||
*/
|
||||
public class ApkVerifier {
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* The verification result also includes errors, warnings, and information about signers.
|
||||
*
|
||||
* @param apk APK file contents
|
||||
* @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures
|
||||
* may need to be verified
|
||||
*
|
||||
* @throws IOException if an I/O error is encountered while reading the APK
|
||||
* @throws ZipFormatException if the APK is malformed at ZIP format level
|
||||
*/
|
||||
public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException {
|
||||
ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
|
||||
|
||||
// Attempt to verify the APK using APK Signature Scheme v2
|
||||
Result result = new Result();
|
||||
try {
|
||||
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
|
||||
result.mergeFrom(v2Result);
|
||||
} catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
|
||||
if (result.containsErrors()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Verify JAR signature if necessary
|
||||
if (!result.isVerifiedUsingV2Scheme()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verified
|
||||
result.setVerified();
|
||||
for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
|
||||
result.addSignerCertificate(signerInfo.getCertificate());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of verifying an APKs signatures. The APK can be considered verified iff
|
||||
* {@link #isVerified()} returns {@code true}.
|
||||
*/
|
||||
public static class Result {
|
||||
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
||||
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
||||
private final List<X509Certificate> mSignerCerts = new ArrayList<>();
|
||||
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
|
||||
|
||||
private boolean mVerified;
|
||||
private boolean mVerifiedUsingV2Scheme;
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK's signatures verified.
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return mVerified;
|
||||
}
|
||||
|
||||
private void setVerified() {
|
||||
mVerified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
|
||||
*/
|
||||
public boolean isVerifiedUsingV2Scheme() {
|
||||
return mVerifiedUsingV2Scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the verified signers' certificates, one per signer.
|
||||
*/
|
||||
public List<X509Certificate> getSignerCertificates() {
|
||||
return mSignerCerts;
|
||||
}
|
||||
|
||||
private void addSignerCertificate(X509Certificate cert) {
|
||||
mSignerCerts.add(cert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about APK Signature Scheme v2 signers associated with the APK's
|
||||
* signature.
|
||||
*/
|
||||
public List<V2SchemeSignerInfo> getV2SchemeSigners() {
|
||||
return mV2SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns errors encountered while verifying the APK's signatures.
|
||||
*/
|
||||
public List<IssueWithParams> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns warnings encountered while verifying the APK's signatures.
|
||||
*/
|
||||
public List<IssueWithParams> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
private void mergeFrom(V2SchemeVerifier.Result source) {
|
||||
mVerifiedUsingV2Scheme = source.verified;
|
||||
mErrors.addAll(source.getErrors());
|
||||
mWarnings.addAll(source.getWarnings());
|
||||
for (V2SchemeVerifier.Result.SignerInfo signer : source.signers) {
|
||||
mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if an error was encountered while verifying the APK. Any error
|
||||
* prevents the APK from being considered verified.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mV2SchemeSigners.isEmpty()) {
|
||||
for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about an APK Signature Scheme v2 signer associated with the APK's signature.
|
||||
*/
|
||||
public static class V2SchemeSignerInfo {
|
||||
private final int mIndex;
|
||||
private final List<X509Certificate> mCerts;
|
||||
|
||||
private final List<IssueWithParams> mErrors;
|
||||
private final List<IssueWithParams> mWarnings;
|
||||
|
||||
private V2SchemeSignerInfo(V2SchemeVerifier.Result.SignerInfo result) {
|
||||
mIndex = result.index;
|
||||
mCerts = result.certs;
|
||||
mErrors = result.getErrors();
|
||||
mWarnings = result.getWarnings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this signer's {@code 0}-based index in the list of signers contained in the
|
||||
* APK's APK Signature Scheme v2 signature.
|
||||
*/
|
||||
public int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 mCerts.isEmpty() ? null : mCerts.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this signer's certificates. The first certificate is for the signer's public
|
||||
* key. An empty list may be returned if an error was encountered during verification
|
||||
* (see {@link #containsErrors()}).
|
||||
*/
|
||||
public List<X509Certificate> getCertificates() {
|
||||
return mCerts;
|
||||
}
|
||||
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
|
||||
public List<IssueWithParams> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
public List<IssueWithParams> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error or warning encountered while verifying an APK's signatures.
|
||||
*/
|
||||
public static enum Issue {
|
||||
|
||||
/**
|
||||
* Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
|
||||
*/
|
||||
V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
|
||||
|
||||
/**
|
||||
* Failed to parse this signer's signer block contained in the APK Signature Scheme v2
|
||||
* signature.
|
||||
*/
|
||||
V2_SIG_MALFORMED_SIGNER("Malformed signer block"),
|
||||
|
||||
/**
|
||||
* Public key embedded in the APK Signature Scheme v2 signature of this signer could not be
|
||||
* parsed.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: error details ({@code Throwable})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer's certificate could not be parsed.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
|
||||
* certificates ({@code Integer})</li>
|
||||
* <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
|
||||
* list of certificates ({@code Integer})</li>
|
||||
* <li>Parameter 3: error details ({@code Throwable})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
|
||||
|
||||
/**
|
||||
* Failed to parse this signer's signature record contained in the APK Signature Scheme v2
|
||||
* signature.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"),
|
||||
|
||||
/**
|
||||
* Failed to parse this signer's digest record contained in the APK Signature Scheme v2
|
||||
* signature.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer contains a malformed additional attribute.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 signature contains no signers.
|
||||
*/
|
||||
V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer contains a signature produced using an unknown
|
||||
* algorithm.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: algorithm ID ({@code Integer})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer contains an unknown additional attribute.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: attribute ID ({@code Integer})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
|
||||
|
||||
/**
|
||||
* An exception was encountered while verifying APK Signature Scheme v2 signature of this
|
||||
* signer.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
|
||||
* <li>Parameter 2: exception ({@code Throwable})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 signature over this signer's signed-data block did not verify.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer offers no signatures.
|
||||
*/
|
||||
V2_SIG_NO_SIGNATURES("No signatures"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer offers signatures but none of them are supported.
|
||||
*/
|
||||
V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer offers no certificates.
|
||||
*/
|
||||
V2_SIG_NO_CERTIFICATES("No certificates"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer's public key listed in the signer's certificate does
|
||||
* not match the public key listed in the signatures record.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
|
||||
* <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
|
||||
"Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
|
||||
|
||||
/**
|
||||
* This APK Signature Scheme v2 signer's signature algorithms listed in the signatures
|
||||
* record do not match the signature algorithms listed in the signatures record.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
|
||||
* <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
|
||||
"Signature algorithms mismatch between signatures and digests records"
|
||||
+ ": %1$s vs %2$s"),
|
||||
|
||||
/**
|
||||
* The APK's digest does not match the digest contained in the APK Signature Scheme v2
|
||||
* signature.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
|
||||
* <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
|
||||
* <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
|
||||
* </ul>
|
||||
*/
|
||||
V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
|
||||
"APK integrity check failed. %1$s digest mismatch."
|
||||
+ " Expected: <%2$s>, actual: <%3$s>"),
|
||||
|
||||
/**
|
||||
* APK Signing Block contains an unknown entry.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Parameter 1: entry ID ({@code Integer})</li>
|
||||
* </ul>
|
||||
*/
|
||||
APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
|
||||
|
||||
private final String mFormat;
|
||||
|
||||
private Issue(String format) {
|
||||
mFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format string suitable for combining the parameters of this issue into a
|
||||
* readable string. See {@link java.util.Formatter} for format.
|
||||
*/
|
||||
private String getFormat() {
|
||||
return mFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
|
||||
* form.
|
||||
*/
|
||||
public static class IssueWithParams {
|
||||
private final Issue mIssue;
|
||||
private final Object[] mParams;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code IssueWithParams} of the specified type and with provided
|
||||
* parameters.
|
||||
*/
|
||||
public IssueWithParams(Issue issue, Object[] params) {
|
||||
mIssue = issue;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of this issue.
|
||||
*/
|
||||
public Issue getIssue() {
|
||||
return mIssue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters of this issue.
|
||||
*/
|
||||
public Object[] getParams() {
|
||||
return mParams.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a readable form of this issue.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(mIssue.getFormat(), mParams);
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,9 +18,9 @@ package com.android.apksigner.core;
|
||||
|
||||
import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm;
|
||||
import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner;
|
||||
import com.android.apksigner.core.internal.apk.v2.MessageDigestSink;
|
||||
import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner;
|
||||
import com.android.apksigner.core.internal.util.ByteArrayOutputStreamSink;
|
||||
import com.android.apksigner.core.internal.util.MessageDigestSink;
|
||||
import com.android.apksigner.core.internal.util.Pair;
|
||||
import com.android.apksigner.core.util.DataSink;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
|
@@ -19,7 +19,7 @@ package com.android.apksigner.core.internal.apk.v2;
|
||||
/**
|
||||
* APK Signature Scheme v2 content digest algorithm.
|
||||
*/
|
||||
enum ContentDigestAlgorithm {
|
||||
public enum ContentDigestAlgorithm {
|
||||
/** SHA2-256 over 1 MB chunks. */
|
||||
CHUNKED_SHA256("SHA-256", 256 / 8),
|
||||
|
||||
|
@@ -23,7 +23,7 @@ import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 content digest algorithm.
|
||||
* APK Signature Scheme v2 signature algorithm.
|
||||
*/
|
||||
public enum SignatureAlgorithm {
|
||||
/**
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.apksigner.core.internal.apk.v2;
|
||||
|
||||
import com.android.apksigner.core.internal.util.MessageDigestSink;
|
||||
import com.android.apksigner.core.internal.util.Pair;
|
||||
import com.android.apksigner.core.internal.zip.ZipUtils;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
@@ -216,7 +217,7 @@ public abstract class V2SchemeSigner {
|
||||
return generateApkSigningBlock(signerConfigs, contentDigests);
|
||||
}
|
||||
|
||||
private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
|
||||
static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
|
||||
Set<ContentDigestAlgorithm> digestAlgorithms,
|
||||
DataSource[] contents) throws IOException, DigestException {
|
||||
// For each digest algorithm the result is computed as follows:
|
||||
|
@@ -0,0 +1,939 @@
|
||||
/*
|
||||
* 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.apk.v2;
|
||||
|
||||
import com.android.apksigner.core.ApkVerifier.Issue;
|
||||
import com.android.apksigner.core.ApkVerifier.IssueWithParams;
|
||||
import com.android.apksigner.core.apk.ApkUtils;
|
||||
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
|
||||
import com.android.apksigner.core.internal.util.DelegatingX509Certificate;
|
||||
import com.android.apksigner.core.internal.util.Pair;
|
||||
import com.android.apksigner.core.internal.zip.ZipUtils;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.DigestException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 verifier.
|
||||
*
|
||||
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||
* uncompressed contents of ZIP entries.
|
||||
*
|
||||
* <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
|
||||
*/
|
||||
public abstract class V2SchemeVerifier {
|
||||
|
||||
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
|
||||
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
|
||||
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
|
||||
|
||||
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SchemeVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
|
||||
* verification. APK is considered verified only if {@link Result#verified} is {@code true}. If
|
||||
* verification fails, the result will contain errors -- see {@link Result#getErrors()}.
|
||||
*
|
||||
* @throws SignatureNotFoundException if no APK Signature Scheme v2 signatures are found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
Result result = new Result();
|
||||
SignatureInfo signatureInfo = findSignature(apk, zipSections, result);
|
||||
|
||||
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
|
||||
DataSource centralDir =
|
||||
apk.slice(
|
||||
signatureInfo.centralDirOffset,
|
||||
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
||||
ByteBuffer eocd = signatureInfo.eocd;
|
||||
|
||||
verify(beforeApkSigningBlock,
|
||||
signatureInfo.signatureBlock,
|
||||
centralDir,
|
||||
eocd,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's v2 signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the
|
||||
* {@code result}.
|
||||
*/
|
||||
private static void verify(
|
||||
DataSource beforeApkSigningBlock,
|
||||
ByteBuffer apkSignatureSchemeV2Block,
|
||||
DataSource centralDir,
|
||||
ByteBuffer eocd,
|
||||
Result result) throws IOException {
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||
parseSigners(apkSignatureSchemeV2Block, contentDigestsToVerify, result);
|
||||
if (result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
verifyIntegrity(
|
||||
beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
|
||||
if (!result.containsErrors()) {
|
||||
result.verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the provided APK Signature Scheme v2 block and populates
|
||||
* {@code signerInfos} of the provided {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
||||
*/
|
||||
private static void parseSigners(
|
||||
ByteBuffer apkSignatureSchemeV2Block,
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||
Result result) {
|
||||
ByteBuffer signers;
|
||||
try {
|
||||
signers = getLengthPrefixedSlice(apkSignatureSchemeV2Block);
|
||||
} catch (IOException e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
|
||||
return;
|
||||
}
|
||||
if (!signers.hasRemaining()) {
|
||||
result.addError(Issue.V2_SIG_NO_SIGNERS);
|
||||
return;
|
||||
}
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
int signerCount = 0;
|
||||
while (signers.hasRemaining()) {
|
||||
int signerIndex = signerCount;
|
||||
signerCount++;
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
signerInfo.index = signerIndex;
|
||||
result.signers.add(signerInfo);
|
||||
try {
|
||||
ByteBuffer signer = getLengthPrefixedSlice(signers);
|
||||
parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
|
||||
} catch (IOException | BufferUnderflowException e) {
|
||||
signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signer block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||
* verify the integrity of the rest of the APK. Rather, this method adds to the
|
||||
* {@code contentDigestsToVerify}.
|
||||
*/
|
||||
private static void parseSigner(
|
||||
ByteBuffer signerBlock,
|
||||
CertificateFactory certFactory,
|
||||
Result.SignerInfo result,
|
||||
Set<ContentDigestAlgorithm> contentDigestsToVerify) throws IOException {
|
||||
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
|
||||
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||
signedData.get(signedDataBytes);
|
||||
signedData.flip();
|
||||
result.signedData = signedDataBytes;
|
||||
|
||||
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
|
||||
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
|
||||
|
||||
// Parse the signatures block and identify supported signatures
|
||||
int signatureCount = 0;
|
||||
List<SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||
while (signatures.hasRemaining()) {
|
||||
signatureCount++;
|
||||
try {
|
||||
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||
int sigAlgorithmId = signature.getInt();
|
||||
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||
result.signatures.add(
|
||||
new Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||
continue;
|
||||
}
|
||||
supportedSignatures.add(new SupportedSignature(signatureAlgorithm, sigBytes));
|
||||
} catch (IOException | BufferUnderflowException e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (result.signatures.isEmpty()) {
|
||||
result.addError(Issue.V2_SIG_NO_SIGNATURES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify signatures over signed-data block using the public key
|
||||
List<SupportedSignature> signaturesToVerify = getSignaturesToVerify(supportedSignatures);
|
||||
if (signaturesToVerify.isEmpty()) {
|
||||
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
|
||||
return;
|
||||
}
|
||||
for (SupportedSignature signature : signaturesToVerify) {
|
||||
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||
String jcaSignatureAlgorithm =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey =
|
||||
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||
new X509EncodedKeySpec(publicKeyBytes));
|
||||
} catch (Exception e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
signedData.position(0);
|
||||
sig.update(signedData);
|
||||
byte[] sigBytes = signature.signature;
|
||||
if (!sig.verify(sigBytes)) {
|
||||
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||
return;
|
||||
}
|
||||
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||
} catch (Exception e) {
|
||||
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||
signedData.position(0);
|
||||
ByteBuffer digests = getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
|
||||
|
||||
// Parse the certificates block
|
||||
int certificateIndex = -1;
|
||||
while (certificates.hasRemaining()) {
|
||||
certificateIndex++;
|
||||
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
|
||||
X509Certificate certificate;
|
||||
try {
|
||||
certificate =
|
||||
(X509Certificate)
|
||||
certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
} catch (CertificateException e) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_MALFORMED_CERTIFICATE,
|
||||
certificateIndex,
|
||||
certificateIndex + 1,
|
||||
e);
|
||||
return;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is becase some X509Certificate(Factory) implementations re-encode
|
||||
// certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||
result.certs.add(certificate);
|
||||
}
|
||||
|
||||
if (result.certs.isEmpty()) {
|
||||
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
|
||||
return;
|
||||
}
|
||||
X509Certificate mainCertificate = result.certs.get(0);
|
||||
byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||
toHex(certificatePublicKeyBytes),
|
||||
toHex(publicKeyBytes));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the digests block
|
||||
int digestCount = 0;
|
||||
while (digests.hasRemaining()) {
|
||||
digestCount++;
|
||||
try {
|
||||
ByteBuffer digest = getLengthPrefixedSlice(digests);
|
||||
int sigAlgorithmId = digest.getInt();
|
||||
byte[] digestBytes = readLengthPrefixedByteArray(digest);
|
||||
result.contentDigests.add(
|
||||
new Result.SignerInfo.ContentDigest(sigAlgorithmId, digestBytes));
|
||||
} catch (IOException | BufferUnderflowException e) {
|
||||
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
||||
for (Result.SignerInfo.Signature signature : result.signatures) {
|
||||
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
||||
}
|
||||
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
||||
for (Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
||||
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
||||
}
|
||||
|
||||
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
||||
sigAlgsFromSignaturesRecord,
|
||||
sigAlgsFromDigestsRecord);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the additional attributes block.
|
||||
int additionalAttributeCount = 0;
|
||||
while (additionalAttributes.hasRemaining()) {
|
||||
additionalAttributeCount++;
|
||||
try {
|
||||
ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
|
||||
int id = attribute.getInt();
|
||||
byte[] value = readLengthPrefixedByteArray(attribute);
|
||||
result.additionalAttributes.add(
|
||||
new Result.SignerInfo.AdditionalAttribute(id, value));
|
||||
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||
} catch (IOException | BufferUnderflowException e) {
|
||||
result.addError(
|
||||
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SupportedSignature> getSignaturesToVerify(
|
||||
List<SupportedSignature> signatures) {
|
||||
// Pick the signature with the strongest algorithm, to mimic Android's behavior.
|
||||
SignatureAlgorithm bestSigAlgorithm = null;
|
||||
byte[] bestSigAlgorithmSignatureBytes = null;
|
||||
for (SupportedSignature sig : signatures) {
|
||||
SignatureAlgorithm sigAlgorithm = sig.algorithm;
|
||||
if ((bestSigAlgorithm == null)
|
||||
|| (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
|
||||
bestSigAlgorithm = sigAlgorithm;
|
||||
bestSigAlgorithmSignatureBytes = sig.signature;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSigAlgorithm == null) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
return Collections.singletonList(
|
||||
new SupportedSignature(bestSigAlgorithm, bestSigAlgorithmSignatureBytes));
|
||||
}
|
||||
}
|
||||
|
||||
private static class SupportedSignature {
|
||||
private final SignatureAlgorithm algorithm;
|
||||
private final byte[] signature;
|
||||
|
||||
private SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
|
||||
this.algorithm = algorithm;
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
||||
*/
|
||||
private static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
|
||||
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
|
||||
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
|
||||
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
||||
*/
|
||||
private static int compareContentDigestAlgorithm(
|
||||
ContentDigestAlgorithm alg1,
|
||||
ContentDigestAlgorithm alg2) {
|
||||
switch (alg1) {
|
||||
case CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case CHUNKED_SHA512:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 1;
|
||||
case CHUNKED_SHA512:
|
||||
return 0;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg1: " + alg1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
|
||||
* APK and comparing them against the digests listed in APK Signing Block. The expected digests
|
||||
* taken from {@code v2SchemeSignerInfos} of the provided {@code result}.
|
||||
*/
|
||||
private static void verifyIntegrity(
|
||||
DataSource beforeApkSigningBlock,
|
||||
DataSource centralDir,
|
||||
ByteBuffer eocd,
|
||||
Set<ContentDigestAlgorithm> contentDigestAlgorithms,
|
||||
Result result) throws IOException {
|
||||
if (contentDigestAlgorithms.isEmpty()) {
|
||||
// This should never occur because this method is invoked once at least one signature
|
||||
// is verified, meaning at least one content digest is known.
|
||||
throw new RuntimeException("No content digests found");
|
||||
}
|
||||
|
||||
// For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
|
||||
// treated as though its Central Directory offset points to the start of APK Signing Block.
|
||||
// We thus modify the EoCD accordingly.
|
||||
ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
|
||||
modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
modifiedEocd.put(eocd);
|
||||
modifiedEocd.flip();
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
|
||||
Map<ContentDigestAlgorithm, byte[]> actualContentDigests;
|
||||
try {
|
||||
actualContentDigests =
|
||||
V2SchemeSigner.computeContentDigests(
|
||||
contentDigestAlgorithms,
|
||||
new DataSource[] {
|
||||
beforeApkSigningBlock,
|
||||
centralDir,
|
||||
new ByteBufferDataSource(modifiedEocd)
|
||||
});
|
||||
} catch (DigestException e) {
|
||||
throw new RuntimeException("Failed to compute content digests", e);
|
||||
}
|
||||
if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
|
||||
throw new RuntimeException(
|
||||
"Mismatch between sets of requested and computed content digests"
|
||||
+ " . Requested: " + contentDigestAlgorithms
|
||||
+ ", computed: " + actualContentDigests.keySet());
|
||||
}
|
||||
|
||||
// Compare digests computed over the rest of APK against the corresponding expected digests
|
||||
// in signer blocks.
|
||||
for (Result.SignerInfo signerInfo : result.signers) {
|
||||
for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
|
||||
SignatureAlgorithm signatureAlgorithm =
|
||||
SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
|
||||
if (signatureAlgorithm == null) {
|
||||
continue;
|
||||
}
|
||||
ContentDigestAlgorithm contentDigestAlgorithm =
|
||||
signatureAlgorithm.getContentDigestAlgorithm();
|
||||
byte[] expectedDigest = expected.getValue();
|
||||
byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
|
||||
if (!Arrays.equals(expectedDigest, actualDigest)) {
|
||||
signerInfo.addError(
|
||||
Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
|
||||
contentDigestAlgorithm,
|
||||
toHex(expectedDigest),
|
||||
toHex(actualDigest));
|
||||
continue;
|
||||
}
|
||||
signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 block and additional information relevant to verifying the signatures
|
||||
* contained in the block against the file.
|
||||
*/
|
||||
private static class SignatureInfo {
|
||||
/** Contents of APK Signature Scheme v2 block. */
|
||||
private final ByteBuffer signatureBlock;
|
||||
|
||||
/** Position of the APK Signing Block in the file. */
|
||||
private final long apkSigningBlockOffset;
|
||||
|
||||
/** Position of the ZIP Central Directory in the file. */
|
||||
private final long centralDirOffset;
|
||||
|
||||
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
||||
private final long eocdOffset;
|
||||
|
||||
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||
private final ByteBuffer eocd;
|
||||
|
||||
private SignatureInfo(
|
||||
ByteBuffer signatureBlock,
|
||||
long apkSigningBlockOffset,
|
||||
long centralDirOffset,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
this.signatureBlock = signatureBlock;
|
||||
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||
this.centralDirOffset = centralDirOffset;
|
||||
this.eocdOffset = eocdOffset;
|
||||
this.eocd = eocd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the APK Signature Scheme v2 block contained in the provided APK file and the
|
||||
* additional information relevant for verifying the block against the file.
|
||||
*
|
||||
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2
|
||||
* @throws IOException if an I/O error occurs while reading the APK
|
||||
*/
|
||||
private static SignatureInfo findSignature(
|
||||
DataSource apk, ApkUtils.ZipSections zipSections, Result result)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
|
||||
long centralDirEndOffset =
|
||||
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
||||
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
|
||||
if (centralDirEndOffset != eocdStartOffset) {
|
||||
throw new SignatureNotFoundException(
|
||||
"ZIP Central Directory is not immediately followed by End of Central Directory"
|
||||
+ ". CD end: " + centralDirEndOffset
|
||||
+ ", EoCD start: " + eocdStartOffset);
|
||||
}
|
||||
|
||||
// Find the APK Signing Block. The block immediately precedes the Central Directory.
|
||||
ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
|
||||
Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
|
||||
findApkSigningBlock(apk, centralDirStartOffset);
|
||||
ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
|
||||
long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
|
||||
|
||||
// Find the APK Signature Scheme v2 Block inside the APK Signing Block.
|
||||
ByteBuffer apkSignatureSchemeV2Block =
|
||||
findApkSignatureSchemeV2Block(apkSigningBlock, result);
|
||||
|
||||
return new SignatureInfo(
|
||||
apkSignatureSchemeV2Block,
|
||||
apkSigningBlockOffset,
|
||||
centralDirStartOffset,
|
||||
eocdStartOffset,
|
||||
eocd);
|
||||
}
|
||||
|
||||
private static Pair<ByteBuffer, Long> findApkSigningBlock(
|
||||
DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
|
||||
// FORMAT:
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes payload
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
|
||||
if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK too small for APK Signing Block. ZIP Central Directory offset: "
|
||||
+ centralDirOffset);
|
||||
}
|
||||
// Read the magic and offset in file from the footer section of the block:
|
||||
// * uint64: size of block
|
||||
// * 16 bytes: magic
|
||||
ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24);
|
||||
footer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
||||
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
||||
throw new SignatureNotFoundException(
|
||||
"No APK Signing Block before ZIP Central Directory");
|
||||
}
|
||||
// Read and compare size fields
|
||||
long apkSigBlockSizeInFooter = footer.getLong(0);
|
||||
if ((apkSigBlockSizeInFooter < footer.capacity())
|
||||
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
||||
}
|
||||
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
||||
long apkSigBlockOffset = centralDirOffset - totalSize;
|
||||
if (apkSigBlockOffset < 0) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block offset out of range: " + apkSigBlockOffset);
|
||||
}
|
||||
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, totalSize);
|
||||
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
||||
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block sizes in header and footer do not match: "
|
||||
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
||||
}
|
||||
return Pair.of(apkSigBlock, apkSigBlockOffset);
|
||||
}
|
||||
|
||||
private static ByteBuffer findApkSignatureSchemeV2Block(
|
||||
ByteBuffer apkSigningBlock,
|
||||
Result result) throws SignatureNotFoundException {
|
||||
checkByteOrderLittleEndian(apkSigningBlock);
|
||||
// FORMAT:
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes pairs
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
|
||||
|
||||
int entryCount = 0;
|
||||
while (pairs.hasRemaining()) {
|
||||
entryCount++;
|
||||
if (pairs.remaining() < 8) {
|
||||
throw new SignatureNotFoundException(
|
||||
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
|
||||
}
|
||||
long lenLong = pairs.getLong();
|
||||
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount
|
||||
+ " size out of range: " + lenLong);
|
||||
}
|
||||
int len = (int) lenLong;
|
||||
int nextEntryPos = pairs.position() + len;
|
||||
if (len > pairs.remaining()) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount + " size out of range: " + len
|
||||
+ ", available: " + pairs.remaining());
|
||||
}
|
||||
int id = pairs.getInt();
|
||||
if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
|
||||
return getByteBuffer(pairs, len - 4);
|
||||
}
|
||||
result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
|
||||
pairs.position(nextEntryPos);
|
||||
}
|
||||
|
||||
throw new SignatureNotFoundException(
|
||||
"No APK Signature Scheme v2 block in APK Signing Block");
|
||||
}
|
||||
|
||||
private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
public static class SignatureNotFoundException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SignatureNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SignatureNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||
* position of this buffer.
|
||||
*
|
||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||
* {@code size}.
|
||||
*/
|
||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size)
|
||||
throws BufferUnderflowException {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("size: " + size);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int position = source.position();
|
||||
int limit = position + size;
|
||||
if ((limit < position) || (limit > originalLimit)) {
|
||||
throw new BufferUnderflowException();
|
||||
}
|
||||
source.limit(limit);
|
||||
try {
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
source.position(limit);
|
||||
return result;
|
||||
} finally {
|
||||
source.limit(originalLimit);
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
|
||||
if (source.remaining() < 4) {
|
||||
throw new IOException(
|
||||
"Remaining buffer too short to contain length of length-prefixed field."
|
||||
+ " Remaining: " + source.remaining());
|
||||
}
|
||||
int len = source.getInt();
|
||||
if (len < 0) {
|
||||
throw new IllegalArgumentException("Negative length");
|
||||
} else if (len > source.remaining()) {
|
||||
throw new IOException("Length-prefixed field longer than remaining buffer."
|
||||
+ " Field length: " + len + ", remaining: " + source.remaining());
|
||||
}
|
||||
return getByteBuffer(source, len);
|
||||
}
|
||||
|
||||
private static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
|
||||
int len = buf.getInt();
|
||||
if (len < 0) {
|
||||
throw new IOException("Negative length");
|
||||
} else if (len > buf.remaining()) {
|
||||
throw new IOException("Underflow while reading length-prefixed value. Length: " + len
|
||||
+ ", available: " + buf.remaining());
|
||||
}
|
||||
byte[] result = new byte[len];
|
||||
buf.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
|
||||
* time.
|
||||
*/
|
||||
private static class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
|
||||
private byte[] mEncodedForm;
|
||||
|
||||
public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
|
||||
super(wrapped);
|
||||
this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() throws CertificateEncodingException {
|
||||
return (mEncodedForm != null) ? mEncodedForm.clone() : null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
|
||||
|
||||
private static String toHex(byte[] value) {
|
||||
StringBuilder sb = new StringBuilder(value.length * 2);
|
||||
int len = value.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int hi = (value[i] & 0xff) >>> 4;
|
||||
int lo = value[i] & 0x0f;
|
||||
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
|
||||
/** Whether the APK's APK Signature Scheme v2 signature verifies. */
|
||||
public boolean verified;
|
||||
|
||||
public final List<SignerInfo> signers = new ArrayList<>();
|
||||
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
||||
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
||||
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!signers.isEmpty()) {
|
||||
for (SignerInfo signer : signers) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addError(Issue msg, Object... parameters) {
|
||||
mErrors.add(new IssueWithParams(msg, parameters));
|
||||
}
|
||||
|
||||
public void addWarning(Issue msg, Object... parameters) {
|
||||
mWarnings.add(new IssueWithParams(msg, parameters));
|
||||
}
|
||||
|
||||
public List<IssueWithParams> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
public List<IssueWithParams> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
public static class SignerInfo {
|
||||
public int index;
|
||||
public List<X509Certificate> certs = new ArrayList<>();
|
||||
public List<ContentDigest> contentDigests = new ArrayList<>();
|
||||
public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
|
||||
public List<Signature> signatures = new ArrayList<>();
|
||||
public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
|
||||
public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
|
||||
public byte[] signedData;
|
||||
|
||||
private final List<IssueWithParams> mWarnings = new ArrayList<>();
|
||||
private final List<IssueWithParams> mErrors = new ArrayList<>();
|
||||
|
||||
public void addError(Issue msg, Object... parameters) {
|
||||
mErrors.add(new IssueWithParams(msg, parameters));
|
||||
}
|
||||
|
||||
public void addWarning(Issue msg, Object... parameters) {
|
||||
mWarnings.add(new IssueWithParams(msg, parameters));
|
||||
}
|
||||
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
|
||||
public List<IssueWithParams> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
public List<IssueWithParams> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
public static class ContentDigest {
|
||||
private final int mSignatureAlgorithmId;
|
||||
private final byte[] mValue;
|
||||
|
||||
public ContentDigest(int signatureAlgorithmId, byte[] value) {
|
||||
mSignatureAlgorithmId = signatureAlgorithmId;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getSignatureAlgorithmId() {
|
||||
return mSignatureAlgorithmId;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Signature {
|
||||
private final int mAlgorithmId;
|
||||
private final byte[] mValue;
|
||||
|
||||
public Signature(int algorithmId, byte[] value) {
|
||||
mAlgorithmId = algorithmId;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getAlgorithmId() {
|
||||
return mAlgorithmId;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdditionalAttribute {
|
||||
private final int mId;
|
||||
private final byte[] mValue;
|
||||
|
||||
public AdditionalAttribute(int id, byte[] value) {
|
||||
mId = id;
|
||||
mValue = value.clone();
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return mValue.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Principal;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* {@link X509Certificate} which delegates all method invocations to the provided delegate
|
||||
* {@code X509Certificate}.
|
||||
*/
|
||||
public class DelegatingX509Certificate extends X509Certificate {
|
||||
private final X509Certificate mDelegate;
|
||||
|
||||
public DelegatingX509Certificate(X509Certificate delegate) {
|
||||
this.mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getCriticalExtensionOIDs() {
|
||||
return mDelegate.getCriticalExtensionOIDs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getExtensionValue(String oid) {
|
||||
return mDelegate.getExtensionValue(oid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNonCriticalExtensionOIDs() {
|
||||
return mDelegate.getNonCriticalExtensionOIDs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasUnsupportedCriticalExtension() {
|
||||
return mDelegate.hasUnsupportedCriticalExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValidity()
|
||||
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||
mDelegate.checkValidity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValidity(Date date)
|
||||
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||
mDelegate.checkValidity(date);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return mDelegate.getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getSerialNumber() {
|
||||
return mDelegate.getSerialNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getIssuerDN() {
|
||||
return mDelegate.getIssuerDN();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getSubjectDN() {
|
||||
return mDelegate.getSubjectDN();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getNotBefore() {
|
||||
return mDelegate.getNotBefore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getNotAfter() {
|
||||
return mDelegate.getNotAfter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getTBSCertificate() throws CertificateEncodingException {
|
||||
return mDelegate.getTBSCertificate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSignature() {
|
||||
return mDelegate.getSignature();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSigAlgName() {
|
||||
return mDelegate.getSigAlgName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSigAlgOID() {
|
||||
return mDelegate.getSigAlgOID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSigAlgParams() {
|
||||
return mDelegate.getSigAlgParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getIssuerUniqueID() {
|
||||
return mDelegate.getIssuerUniqueID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getSubjectUniqueID() {
|
||||
return mDelegate.getSubjectUniqueID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getKeyUsage() {
|
||||
return mDelegate.getKeyUsage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBasicConstraints() {
|
||||
return mDelegate.getBasicConstraints();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() throws CertificateEncodingException {
|
||||
return mDelegate.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, NoSuchProviderException, SignatureException {
|
||||
mDelegate.verify(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key, String sigProvider)
|
||||
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
NoSuchProviderException, SignatureException {
|
||||
mDelegate.verify(key, sigProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mDelegate.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getPublicKey() {
|
||||
return mDelegate.getPublicKey();
|
||||
}
|
||||
}
|
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksigner.core.internal.apk.v2;
|
||||
package com.android.apksigner.core.internal.util;
|
||||
|
||||
import com.android.apksigner.core.util.DataSink;
|
||||
|
@@ -20,7 +20,6 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import com.android.apksigner.core.internal.util.ByteBufferSink;
|
||||
import com.android.apksigner.core.internal.util.Pair;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
|
||||
@@ -175,11 +174,10 @@ public abstract class ZipUtils {
|
||||
// Lower maxCommentSize if the file is too small.
|
||||
maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
|
||||
int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
|
||||
long bufOffsetInFile = fileSize - maxEocdSize;
|
||||
ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long bufOffsetInFile = fileSize - buf.capacity();
|
||||
zip.feed(bufOffsetInFile, buf.remaining(), new ByteBufferSink(buf));
|
||||
buf.flip();
|
||||
int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
|
||||
if (eocdOffsetInBuf == -1) {
|
||||
// No EoCD record found in the buffer
|
||||
@@ -252,10 +250,8 @@ public abstract class ZipUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBuffer sig = ByteBuffer.allocate(4);
|
||||
ByteBuffer sig = zip.getByteBuffer(locatorPosition, 4);
|
||||
sig.order(ByteOrder.LITTLE_ENDIAN);
|
||||
zip.feed(locatorPosition, sig.remaining(), new ByteBufferSink(sig));
|
||||
sig.flip();
|
||||
return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user