summaryrefslogtreecommitdiff
path: root/src/main/java/com/android
diff options
context:
space:
mode:
authorAlex Klyubin <klyubin@google.com>2016-05-04 15:27:46 -0700
committerAlex Klyubin <klyubin@google.com>2016-12-12 12:41:26 -0800
commita96ce3418bbd07c9df96e15698a05f26b3e7d9ce (patch)
tree57f0f46a6ee4bc526c2cd1bf77f3eeeaa5441d3c /src/main/java/com/android
parent6b7c4e9243cc2a224fbc6cb7f79edf9c06cbc9a0 (diff)
downloadapkzlib-a96ce3418bbd07c9df96e15698a05f26b3e7d9ce.tar.gz
Use apksig's ApkSignerEngine for signing APKs
This switches the new APK build pipeline from its own logic for signing APKs to the logic provided by DefaultApkSignerEngine primitive from apksig library. The switch to apksig library also fixes two bugs in the APK signing pipeline: 1. digests of named sections of MANIFEST.MF are now correct in .SF files, and 2. signatures of DSA-signed APKs will now verify on Android platforms with API Level 8 and lower. Test: ./gradlew :base:integration-test:test --tests *SigningTest Test: ./tools/base/bazel/bazel build //tools/base/build-system/builder:builder_maven Bug: 31060257 Bug: 31306164 Change-Id: I742c1362a9eff63dc009a5419636041341cf7309
Diffstat (limited to 'src/main/java/com/android')
-rw-r--r--src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java29
-rw-r--r--src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java211
-rw-r--r--src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java106
-rw-r--r--src/main/java/com/android/apkzlib/sign/SignatureExtension.java630
-rw-r--r--src/main/java/com/android/apkzlib/sign/SigningExtension.java392
-rw-r--r--src/main/java/com/android/apkzlib/sign/ZFileDataSource.java157
-rw-r--r--src/main/java/com/android/apkzlib/sign/package-info.java33
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java596
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java59
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java52
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/DigestSource.java50
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java114
-rw-r--r--src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java68
-rw-r--r--src/main/java/com/android/apkzlib/zfile/ZFiles.java39
-rw-r--r--src/main/java/com/android/apkzlib/zip/StoredEntry.java7
-rw-r--r--src/main/java/com/android/apkzlib/zip/ZFile.java85
16 files changed, 657 insertions, 1971 deletions
diff --git a/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
index 6833d2f..64427ae 100644
--- a/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
@@ -40,14 +40,15 @@ public enum DigestAlgorithm {
SHA256("SHA-256", "SHA-256");
/**
- * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA}.
+ * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and
+ * {@link SignatureAlgorithm#ECDSA}.
*/
- public static final int API_SHA_256_RSA = 18;
+ public static final int API_SHA_256_RSA_AND_ECDSA = 18;
/**
* API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s.
*
- * <p>Before that, SHA256 can only be used with RSA.
+ * <p>Before that, SHA256 can only be used with RSA and ECDSA.
*/
public static final int API_SHA_256_ALL_ALGORITHMS = 21;
@@ -80,26 +81,4 @@ public enum DigestAlgorithm {
this.entryAttributeName = attributeName + "-Digest";
this.manifestAttributeName = attributeName + "-Digest-Manifest";
}
-
- /**
- * Finds the best digest algorithm applicable for a given SDK.
- *
- * @param minSdk the minimum SDK
- * @param signatureAlgorithm signature algorithm used
- * @return the best algorithm found
- */
- @Nonnull
- public static DigestAlgorithm findBest(
- int minSdk,
- @Nonnull SignatureAlgorithm signatureAlgorithm) {
- if (signatureAlgorithm == SignatureAlgorithm.RSA) {
- // PKCS #7 RSA signatures with SHA-256 are
- // supported only since API Level 18 (JB MR2).
- return minSdk >= API_SHA_256_RSA ? SHA256 : SHA1;
- } else {
- // PKCS #7 ECDSA and DSA signatures with SHA-256
- // are supported only since API Level 21 (Android L).
- return minSdk >= API_SHA_256_ALL_ALGORITHMS ? SHA256 : SHA1;
- }
- }
}
diff --git a/src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java b/src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java
deleted file mode 100644
index ef5f547..0000000
--- a/src/main/java/com/android/apkzlib/sign/FullApkSignExtension.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * 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.apkzlib.sign;
-
-import com.android.apkzlib.sign.v2.ApkSignerV2;
-import com.android.apkzlib.sign.v2.ByteArrayDigestSource;
-import com.android.apkzlib.sign.v2.DigestSource;
-import com.android.apkzlib.sign.v2.ZFileDigestSource;
-import com.android.apkzlib.utils.IOExceptionRunnable;
-import com.android.apkzlib.zip.StoredEntry;
-import com.android.apkzlib.zip.ZFile;
-import com.android.apkzlib.zip.ZFileExtension;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.PrivateKey;
-import java.security.SignatureException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-/**
- * Extension that adds full APK signing. This extension will:
- * <ul>
- * <li>Generate a new signature if the zip is modified after the extension is added.</li>
- * <li>Generate a new signature even if the zip is not modified after the extension is
- * added, but a valid signature is not found.</li>
- * </ul>
- * <p>
- * The extension computes the signature, if needed, at the {@link ZFileExtension#entriesWritten()}
- * event, after all zip entries have been written, but before the central directory or EOCD have.
- * This allows the extension to set the central directory offset parameter in the zip file
- * using {@link ZFile#setExtraDirectoryOffset(long)} adding enough space in the zip file for the
- * signature block.
- * <p>
- * The signature block is written before the central directory, allowing the zip file to be written
- * in sequential order.
- */
-public class FullApkSignExtension {
-
- /**
- * The zip file this extension is registered with.
- */
- @Nonnull
- private final ZFile mFile;
-
- /**
- * Signer certificate.
- */
- @Nonnull
- private final X509Certificate mCertificate;
-
- /**
- * Signer private key.
- */
- @Nonnull
- private final PrivateKey mPrivateKey;
-
- /**
- * APK Signature Scheme v2 algorithms to use for signing the APK.
- */
- private final List<com.android.apkzlib.sign.v2.SignatureAlgorithm> mV2SignatureAlgorithms;
-
- /**
- * {@code true} if the zip needs its signature to be updated.
- */
- private boolean mNeedsSignatureUpdate = true;
-
- /**
- * The extension to register with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new extension. This will not register the extension with the provided
- * {@link ZFile}. Until {@link #register()} is invoked, this extension is not used.
- *
- * @param file the zip file to register the extension with
- * @param minSdkVersion minSdkVersion of the package
- * @param certificate sign certificate
- * @param privateKey the private key to sign the jar
- *
- * @throws InvalidKeyException if the signing key is not suitable for signing this APK.
- */
- public FullApkSignExtension(@Nonnull ZFile file,
- int minSdkVersion,
- @Nonnull X509Certificate certificate,
- @Nonnull PrivateKey privateKey) throws InvalidKeyException {
- mFile = file;
- mCertificate = certificate;
- mPrivateKey = privateKey;
- mV2SignatureAlgorithms =
- ApkSignerV2.getSuggestedSignatureAlgorithms(
- certificate.getPublicKey(), minSdkVersion);
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the constructor.
- */
- public void register() {
- Preconditions.checkState(mExtension == null, "register() has already been invoked.");
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() throws IOException {
- mFile.setExtraDirectoryOffset(0);
- return null;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable added(@Nonnull StoredEntry entry,
- @Nullable StoredEntry replaced) {
- onZipChanged();
- return null;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
- onZipChanged();
- return null;
- }
-
- @Override
- public void entriesWritten() throws IOException {
- onEntriesWritten();
- }
- };
-
- mFile.addZFileExtension(mExtension);
- }
-
- /**
- * Invoked when the zip file has been changed.
- */
- private void onZipChanged() {
- mNeedsSignatureUpdate = true;
- }
-
- /**
- * Invoked before the zip file has been updated.
- *
- * @throws IOException failed to perform the update
- */
- private void onEntriesWritten() throws IOException {
- if (!mNeedsSignatureUpdate) {
- return;
- }
- mNeedsSignatureUpdate = false;
-
- byte[] apkSigningBlock = generateApkSigningBlock();
- Verify.verify(apkSigningBlock.length > 0, "apkSigningBlock.length == 0");
- mFile.setExtraDirectoryOffset(apkSigningBlock.length);
- long apkSigningBlockOffset =
- mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset();
- mFile.directWrite(apkSigningBlockOffset, apkSigningBlock);
- }
-
- /**
- * Generates a signature for the APK.
- *
- * @return the signature data block
- * @throws IOException failed to generate a signature
- */
- @Nonnull
- private byte[] generateApkSigningBlock() throws IOException {
- byte[] centralDirectoryData = mFile.getCentralDirectoryBytes();
- byte[] eocdData = mFile.getEocdBytes();
-
- ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
- signerConfig.privateKey = mPrivateKey;
- signerConfig.certificates = ImmutableList.of(mCertificate);
- signerConfig.signatureAlgorithms = mV2SignatureAlgorithms;
- DigestSource centralDir = new ByteArrayDigestSource(centralDirectoryData);
- DigestSource eocd = new ByteArrayDigestSource(eocdData);
- DigestSource zipEntries =
- new ZFileDigestSource(
- mFile,
- 0,
- mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset());
- try {
- return ApkSignerV2.generateApkSigningBlock(
- zipEntries,
- centralDir,
- eocd,
- ImmutableList.of(signerConfig));
- } catch (InvalidKeyException | SignatureException e) {
- throw new IOException("Failed to sign APK using APK Signature Scheme v2", e);
- }
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
index 02e8f06..a8f9b98 100644
--- a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
+++ b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
@@ -24,12 +24,10 @@ import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileExtension;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
-import com.google.common.collect.Maps;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.annotation.Nonnull;
@@ -56,12 +54,12 @@ public class ManifestGenerationExtension {
/**
* Name of META-INF directory.
*/
- public static final String META_INF_DIR = "META-INF";
+ private static final String META_INF_DIR = "META-INF";
/**
* Name of the manifest file.
*/
- public static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
+ static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
/**
* Who should be reported as the manifest builder.
@@ -243,104 +241,4 @@ public class ManifestGenerationExtension {
mZFile.add(MANIFEST_NAME, new ByteArrayInputStream(mManifestBytes.get()));
mDirty = false;
}
-
- /**
- * Obtains the {@link ZFile} this extension is associated with. This method can only be invoked
- * after {@link #register(ZFile)} has been invoked.
- *
- * @return the {@link ZFile}
- */
- @Nonnull
- public ZFile zFile() {
- Preconditions.checkNotNull(mZFile, "mZFile == null");
- return mZFile;
- }
-
- /**
- * Obtains the stored entry in the {@link ZFile} that contains the manifest. This method can
- * only be invoked after {@link #register(ZFile)} has been invoked.
- *
- * @return the entry, {@code null} if none
- */
- @Nullable
- public StoredEntry manifestEntry() {
- Preconditions.checkNotNull(mZFile, "mZFile == null");
- return mZFile.get(MANIFEST_NAME);
- }
-
- /**
- * Obtains an attribute of an entry.
- *
- * @param entryName the name of the entry
- * @param attr the name of the attribute
- * @return the attribute value or {@code null} if the entry does not have any attributes or
- * if it doesn't have the specified attribute
- */
- @Nullable
- public String getAttribute(@Nonnull String entryName, @Nonnull String attr) {
- Attributes attrs = mManifest.getAttributes(entryName);
- if (attrs == null) {
- return null;
- }
-
- return attrs.getValue(attr);
- }
-
- /**
- * Sets the value of an attribute of an entry. If this entry's attribute already has the given
- * value, this method does nothing.
- *
- * @param entryName the name of the entry
- * @param attr the name of the attribute
- * @param value the attribute value
- */
- public void setAttribute(@Nonnull String entryName, @Nonnull String attr,
- @Nonnull String value) {
- Attributes attrs = mManifest.getAttributes(entryName);
- if (attrs == null) {
- attrs = new Attributes();
- markDirty();
- mManifest.getEntries().put(entryName, attrs);
- }
-
- String current = attrs.getValue(attr);
- if (!value.equals(current)) {
- attrs.putValue(attr, value);
- markDirty();
- }
- }
-
- /**
- * Obtains the current manifest.
- *
- * @return a byte sequence representation of the manifest that is guaranteed not to change if
- * the manifest is not modified
- * @throws IOException failed to compute the manifest's byte representation
- */
- @Nonnull
- public byte[] getManifestBytes() throws IOException {
- return mManifestBytes.get();
- }
-
- /**
- * Obtains all entries and all attributes they have in the manifest.
- *
- * @return a map that relates entry names to entry attributes
- */
- @Nonnull
- public Map<String, Attributes> allEntries() {
- return Maps.newHashMap(mManifest.getEntries());
- }
-
- /**
- * Removes an entry from the manifest. If no entry exists with the given name, this operation
- * does nothing.
- *
- * @param name the entry's name
- */
- public void removeEntry(@Nonnull String name) {
- if (mManifest.getEntries().remove(name) != null) {
- markDirty();
- }
- }
}
diff --git a/src/main/java/com/android/apkzlib/sign/SignatureExtension.java b/src/main/java/com/android/apkzlib/sign/SignatureExtension.java
deleted file mode 100644
index 49049b6..0000000
--- a/src/main/java/com/android/apkzlib/sign/SignatureExtension.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- * 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.apkzlib.sign;
-
-import com.android.apkzlib.utils.IOExceptionRunnable;
-import com.android.apkzlib.zip.StoredEntry;
-import com.android.apkzlib.zip.ZFile;
-import com.android.apkzlib.zip.ZFileExtension;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Sets;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Locale;
-import java.util.Set;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
-import java.util.stream.Collectors;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import org.bouncycastle.asn1.ASN1InputStream;
-import org.bouncycastle.asn1.DEROutputStream;
-import org.bouncycastle.cert.jcajce.JcaCertStore;
-import org.bouncycastle.cms.CMSException;
-import org.bouncycastle.cms.CMSProcessableByteArray;
-import org.bouncycastle.cms.CMSSignedData;
-import org.bouncycastle.cms.CMSSignedDataGenerator;
-import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
-
-/**
- * {@link ZFile} extension that signs all files in the APK and generates a signature file and a
- * digital signature of the signature file. The extension registers itself automatically with the
- * {@link ZFile} upon creation.
- * <p>
- * The signature extension will recompute signatures of files already in the zip file but won't
- * update the manifest if these signatures match the ones in the manifest.
- * <p>
- * This extension does 4 main tasks: maintaining the digests of all files in the zip in the manifest
- * file, maintaining the digests of all files in the zip in the signature file, maintaining the
- * digest of the manifest in the signature file and maintaining the digital signature file. For
- * performance, the digests and signatures are only computed when needed.
- * <p>
- * These tasks are done at three different moments: when the extension
- * is created, when files are added to the zip and before the zip is updated.
- * When the extension is created: (Note that the manifest's digest is <em>not</em> checked when
- * the extension is created.)
- * <ul>
- * <li>The signature file is read, if one exists.
- * <li>The signature "administrative" info is read and updated if not up-to-date.
- * <li>The digests for entries in the manifest and signature file that do not correspond to
- * any file in the zip are removed.
- * <li>The digests for all entries in the zip are recomputed and updated in the signature file
- * and in the manifest, if needed.
- * </ul>
- * <p>
- * When files are added or removed:
- * <ul>
- * <li>The signature file and manifest are updated to reflect the changes.
- * <li>If the file was added, its digest is computed.
- * </ul>
- * <p>
- * Before updating the zip file:
- * <ul>
- * <li>If a signature file already exists, checks the digest of the manifest and updates the
- * signature file if needed.
- * <li>Creates the signature file if it did not already exist.
- * <li>Recreates the digital signature of the signature file if the signature file was created
- * or updated.
- * </ul>
- */
-public class SignatureExtension {
-
- /**
- * Base of signature files.
- */
- private static final String SIGNATURE_BASE = ManifestGenerationExtension.META_INF_DIR + "/CERT";
-
- /**
- * Path of the signature file.
- */
- private static final String SIGNATURE_FILE = SIGNATURE_BASE + ".SF";
-
- /**
- * Name of attribute with the signature version.
- */
- private static final String SIGNATURE_VERSION_NAME = "Signature-Version";
-
- /**
- * Version of the signature version.
- */
- private static final String SIGNATURE_VERSION_VALUE = "1.0";
-
- /**
- * Name of attribute with the "created by" attribute.
- */
- private static final String SIGNATURE_CREATED_BY_NAME = "Created-By";
-
- /**
- * Value of the "created by" attribute.
- */
- private static final String SIGNATURE_CREATED_BY_VALUE = "1.0 (Android)";
-
- /**
- * Name of the {@code X-Android-APK-Signer} attribute.
- */
- private static final String SIGNATURE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
-
- /**
- * Value of the {@code X-Android-APK-Signer} attribute when the APK is signed with the v2
- * scheme.
- */
- public static final String SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED = "2";
-
- /**
- * Files to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_FILES = Sets.newHashSet(
- ManifestGenerationExtension.MANIFEST_NAME, SIGNATURE_FILE);
-
- /**
- * Same as {@link #IGNORED_FILES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_FILES_LC = Sets.newHashSet(
- IGNORED_FILES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
-
- /**
- * Prefix of files in META-INF to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_PREFIXES = Sets.newHashSet(
- "SIG-");
-
- /**
- * Same as {@link #IGNORED_PREFIXES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_PREFIXES_LC = Sets.newHashSet(
- IGNORED_PREFIXES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
- /**
- * Suffixes of files in META-INF to ignore when signing. See
- * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
- */
- private static final Set<String> IGNORED_SUFFIXES = Sets.newHashSet(
- ".SF", ".DSA", ".RSA", ".EC");
-
- /**
- * Same as {@link #IGNORED_SUFFIXES} but with all names in lower case.
- */
- private static final Set<String> IGNORED_SUFFIXES_LC = Sets.newHashSet(
- IGNORED_SUFFIXES.stream()
- .map(i -> i.toLowerCase(Locale.US))
- .collect(Collectors.toSet()));
-
- /**
- * Extension maintaining the manifest.
- */
- @Nonnull
- private final ManifestGenerationExtension mManifestExtension;
-
- /**
- * Message digest to use.
- */
- @Nonnull
- private final MessageDigest mMessageDigest;
-
- /**
- * Signature file. Note that the signature file is itself a manifest file but it is
- * a different one from the "standard" MANIFEST.MF.
- */
- @Nonnull
- private final Manifest mSignatureFile;
-
- /**
- * Has the signature manifest been changed?
- */
- private boolean mDirty;
-
- /**
- * Signer certificate.
- */
- @Nonnull
- private final X509Certificate mCertificate;
-
- /**
- * The private key used to sign the jar.
- */
- @Nonnull
- private final PrivateKey mPrivateKey;
-
- /**
- * Algorithm with which .SF file is signed.
- */
- @Nonnull
- private final SignatureAlgorithm mSignatureAlgorithm;
-
- /**
- * Digest algorithm to use for MANIFEST.MF and contents of APK entries.
- */
- @Nonnull
- private final DigestAlgorithm mDigestAlgorithm;
-
- /**
- * Value to output for the {@code X-Android-APK-Signed} header or {@code null} if the header
- * should not be output.
- */
- @Nullable
- private final String mApkSignedHeaderValue;
-
- /**
- * The extension registered with the {@link ZFile}. {@code null} if not registered.
- */
- @Nullable
- private ZFileExtension mExtension;
-
- /**
- * Creates a new signature extension.
- *
- * @param manifestExtension the extension maintaining the manifest
- * @param minSdkVersion minSdkVersion of the package
- * @param certificate sign certificate
- * @param privateKey the private key to sign the jar
- * @param apkSignedHeaderValue value of the {@code X-Android-APK-Signed} header to output into
- * the {@code .SF} file or {@code null} if the header should not be output.
- *
- * @throws NoSuchAlgorithmException failed to obtain the digest algorithm.
- */
- public SignatureExtension(@Nonnull ManifestGenerationExtension manifestExtension,
- int minSdkVersion, @Nonnull X509Certificate certificate, @Nonnull PrivateKey privateKey,
- @Nullable String apkSignedHeaderValue)
- throws NoSuchAlgorithmException {
- mManifestExtension = manifestExtension;
- mSignatureFile = new Manifest();
- mDirty = false;
- mCertificate = certificate;
- mPrivateKey = privateKey;
- mApkSignedHeaderValue = apkSignedHeaderValue;
-
- mSignatureAlgorithm =
- SignatureAlgorithm.fromKeyAlgorithm(privateKey.getAlgorithm(), minSdkVersion);
- mDigestAlgorithm = DigestAlgorithm.findBest(minSdkVersion, mSignatureAlgorithm);
- mMessageDigest = MessageDigest.getInstance(mDigestAlgorithm.messageDigestName);
- }
-
- /**
- * Registers the extension with the {@link ZFile} provided in the
- * {@link ManifestGenerationExtension}. Note that the {@code ManifestGenerationExtension}
- * needs to be registered as a precondition for this method.
- *
- * @throws IOException failed to analyze the zip
- */
- public void register() throws IOException {
- Preconditions.checkState(mExtension == null, "register() already invoked");
-
- mExtension = new ZFileExtension() {
- @Nullable
- @Override
- public IOExceptionRunnable beforeUpdate() {
- return SignatureExtension.this::updateSignatureIfNeeded;
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable added(@Nonnull final StoredEntry entry,
- @Nullable final StoredEntry replaced) {
- if (replaced != null) {
- Preconditions.checkArgument(entry.getCentralDirectoryHeader().getName().equals(
- replaced.getCentralDirectoryHeader().getName()));
- }
-
- if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
- return null;
- }
-
- return () -> {
- if (replaced != null) {
- SignatureExtension.this.removed(replaced);
- }
-
- SignatureExtension.this.added(entry);
- };
- }
-
- @Nullable
- @Override
- public IOExceptionRunnable removed(@Nonnull final StoredEntry entry) {
- if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
- return null;
- }
-
- return () -> SignatureExtension.this.removed(entry);
- }
- };
-
- mManifestExtension.zFile().addZFileExtension(mExtension);
- readSignatureFile();
- }
-
- /**
- * Reads the signature file (if any) on the zip file.
- * <p>
- * When this method terminates, we have the following guarantees:
- * <ul>
- * <li>An internal signature manifest exists.</li>
- * <li>All entries in the in-memory signature file exist in the zip file.</li>
- * <li>All entries in the zip file (with the exception of the signature-related files,
- * as specified by https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html)
- * exist in the in-memory signature file.</li>
- * <li>All entries in the in-memory signature file have digests that match their
- * contents in the zip.</li>
- * <li>All entries in the in-memory signature manifest exist also in the manifest file
- * and the digests are the same.</li>
- * <li>The main attributes of the in-memory signature manifest are valid. The manifest's
- * digest has not been verified and may not even exist.</li>
- * <li>If the internal in-memory signature manifest differs in any way from the one
- * written in the file, {@link #mDirty} will be set to {@code true}. Otherwise,
- * {@link #mDirty} will be set to {@code false}.</li>
- * </ul>
- *
- * @throws IOException failed to read the signature file
- */
- private void readSignatureFile() throws IOException {
- boolean needsNewSignature = false;
-
- StoredEntry signatureEntry = mManifestExtension.zFile().get(SIGNATURE_FILE);
- if (signatureEntry != null) {
- byte[] signatureData = signatureEntry.read();
- mSignatureFile.read(new ByteArrayInputStream(signatureData));
-
- Attributes mainAttrs = mSignatureFile.getMainAttributes();
- String versionName = mainAttrs.getValue(SIGNATURE_VERSION_NAME);
- String createdBy = mainAttrs.getValue(SIGNATURE_CREATED_BY_NAME);
- String apkSigned = mainAttrs.getValue(SIGNATURE_ANDROID_APK_SIGNED_NAME);
-
- if (!SIGNATURE_VERSION_VALUE.equals(versionName)
- || !SIGNATURE_CREATED_BY_VALUE.equals(createdBy)
- || mainAttrs.getValue(mDigestAlgorithm.manifestAttributeName) == null
- || !Objects.equal(mApkSignedHeaderValue, apkSigned)) {
- needsNewSignature = true;
- }
- } else {
- needsNewSignature = true;
- }
-
- if (needsNewSignature) {
- Attributes mainAttrs = mSignatureFile.getMainAttributes();
-
- mainAttrs.putValue(SIGNATURE_CREATED_BY_NAME, SIGNATURE_CREATED_BY_VALUE);
- mainAttrs.putValue(SIGNATURE_VERSION_NAME, SIGNATURE_VERSION_VALUE);
- if (mApkSignedHeaderValue != null) {
- mainAttrs.putValue(SIGNATURE_ANDROID_APK_SIGNED_NAME, mApkSignedHeaderValue);
- } else {
- mainAttrs.remove(SIGNATURE_ANDROID_APK_SIGNED_NAME);
- }
-
- mDirty = true;
- }
-
- /*
- * At this point we have a valid in-memory signature file with a valid header. mDirty
- * states whether this is the same as the file-based signature file.
- *
- * Now, check we have the same files in the zip as in the signature file and that all
- * digests match. While we do this, make sure the manifest is also up-do-date.
- *
- * We ignore all signature-related files that exist in the zip that are signature-related.
- * This are defined in the jar format specification.
- */
- Set<StoredEntry> allEntries =
- mManifestExtension.zFile().entries().stream()
- .filter(se -> !isIgnoredFile(se.getCentralDirectoryHeader().getName()))
- .collect(Collectors.toSet());
-
- Set<String> sigEntriesToRemove = Sets.newHashSet(mSignatureFile.getEntries().keySet());
- Set<String> manEntriesToRemove = Sets.newHashSet(mManifestExtension.allEntries().keySet());
- for (StoredEntry se : allEntries) {
- /*
- * Update the entry's digest, if needed.
- */
- setDigestForEntry(se);
-
- /*
- * This entry exists in the file, so remove it from the list of entries to remove
- * from the manifest and signature file.
- */
- sigEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
- manEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
- }
-
- for (String toRemoveInSignature : sigEntriesToRemove) {
- mSignatureFile.getEntries().remove(toRemoveInSignature);
- mDirty = true;
- }
-
- for (String toRemoveInManifest : manEntriesToRemove) {
- mManifestExtension.removeEntry(toRemoveInManifest);
- }
- }
-
- /**
- * This method will recompute the manifest's digest and will update the signature file if the
- * manifest has changed. It then writes the signature file, if dirty for any reason (including
- * from recomputing the manifest's digest).
- *
- * @throws IOException failed to read / write zip data
- */
- private void updateSignatureIfNeeded() throws IOException {
- byte[] manifestData = mManifestExtension.getManifestBytes();
- byte[] manifestDataDigest = mMessageDigest.digest(manifestData);
-
-
- String manifestDataDigestTxt = Base64.getEncoder().encodeToString(manifestDataDigest);
-
- if (!manifestDataDigestTxt.equals(mSignatureFile.getMainAttributes().getValue(
- mDigestAlgorithm.manifestAttributeName))) {
- mSignatureFile
- .getMainAttributes()
- .putValue(mDigestAlgorithm.manifestAttributeName, manifestDataDigestTxt);
- mDirty = true;
- }
-
- if (!mDirty) {
- return;
- }
-
- ByteArrayOutputStream signatureBytes = new ByteArrayOutputStream();
- mSignatureFile.write(signatureBytes);
-
- mManifestExtension.zFile().add(
- SIGNATURE_FILE,
- new ByteArrayInputStream(signatureBytes.toByteArray()));
-
- String digitalSignatureFile = SIGNATURE_BASE + "." + mPrivateKey.getAlgorithm();
- try {
- mManifestExtension.zFile().add(
- digitalSignatureFile,
- new ByteArrayInputStream(computePkcs7Signature(signatureBytes.toByteArray())));
- } catch (CertificateEncodingException | OperatorCreationException | CMSException e) {
- throw new IOException("Failed to digitally sign signature file.", e);
- }
-
- mDirty = false;
- }
-
- /**
- * A new file has been added.
- *
- * @param entry the entry added
- * @throws IOException failed to add the entry to the signature file (or failed to compute the
- * entry's signature)
- */
- private void added(@Nonnull StoredEntry entry) throws IOException {
- setDigestForEntry(entry);
- }
-
- /**
- * Adds / updates the signature for an entry. If this entry has no signature, or its digest
- * doesn't match the one in the signature file (or manifest), it will be updated.
- *
- * @param entry the entry
- * @throws IOException failed to compute the entry's digest
- */
- private void setDigestForEntry(@Nonnull StoredEntry entry) throws IOException {
- String entryName = entry.getCentralDirectoryHeader().getName();
- byte[] entryDigestArray = mMessageDigest.digest(entry.read());
- String entryDigest = Base64.getEncoder().encodeToString(entryDigestArray);
-
- Attributes signatureAttributes = mSignatureFile.getEntries().get(entryName);
- if (signatureAttributes == null) {
- signatureAttributes = new Attributes();
- mSignatureFile.getEntries().put(entryName, signatureAttributes);
- mDirty = true;
- }
-
- if (!entryDigest.equals(signatureAttributes.getValue(
- mDigestAlgorithm.entryAttributeName))) {
- signatureAttributes.putValue(mDigestAlgorithm.entryAttributeName, entryDigest);
- mDirty = true;
- }
-
- /*
- * setAttribute will not mark the manifest as changed if the attribute is already there
- * and with the same value.
- */
- mManifestExtension.setAttribute(entryName, mDigestAlgorithm.entryAttributeName,
- entryDigest);
- }
-
- /**
- * File has been removed.
- *
- * @param entry the entry removed
- */
- private void removed(@Nonnull StoredEntry entry) {
- mSignatureFile.getEntries().remove(entry.getCentralDirectoryHeader().getName());
- mManifestExtension.removeEntry(entry.getCentralDirectoryHeader().getName());
- mDirty = true;
- }
-
- /**
- * Checks if a file should be ignored when signing.
- *
- * @param name the file name
- * @return should it be ignored
- */
- public static boolean isIgnoredFile(@Nonnull String name) {
- String metaInfPfx = ManifestGenerationExtension.META_INF_DIR + "/";
- boolean inMetaInf = name.startsWith(metaInfPfx)
- && !name.substring(metaInfPfx.length()).contains("/");
-
- /*
- * Only files in META-INF can be ignored. Files in sub-directories of META-INF are not
- * ignored.
- */
- if (!inMetaInf) {
- return false;
- }
-
- String nameLc = name.toLowerCase(Locale.US);
-
- /*
- * All files with names that match (case insensitive) the ignored list are ignored.
- */
- if (IGNORED_FILES_LC.contains(nameLc)) {
- return true;
- }
-
- for (String pfx : IGNORED_PREFIXES_LC) {
- if (nameLc.startsWith(pfx)) {
- return true;
- }
- }
-
- for (String sfx : IGNORED_SUFFIXES_LC) {
- if (nameLc.endsWith(sfx)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Computes the digital signature of an array of data.
- *
- * @param data the data
- * @return the digital signature
- * @throws IOException failed to read/write signature data
- * @throws CertificateEncodingException failed to sign the data
- * @throws OperatorCreationException failed to sign the data
- * @throws CMSException failed to sign the data
- */
- private byte[] computePkcs7Signature(@Nonnull byte[] data) throws IOException,
- CertificateEncodingException, OperatorCreationException, CMSException {
- CMSProcessableByteArray cmsData = new CMSProcessableByteArray(data);
-
- ArrayList<X509Certificate> certList = new ArrayList<>();
- certList.add(mCertificate);
- JcaCertStore certs = new JcaCertStore(certList);
-
- CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
- String signatureAlgName = mSignatureAlgorithm.signatureAlgorithmName(mDigestAlgorithm);
- ContentSigner shaSigner =
- new JcaContentSignerBuilder(signatureAlgName).build(mPrivateKey);
- gen.addSignerInfoGenerator(
- new JcaSignerInfoGeneratorBuilder(
- new JcaDigestCalculatorProviderBuilder()
- .build())
- .setDirectSignature(true)
- .build(shaSigner, mCertificate));
- gen.addCertificates(certs);
- CMSSignedData sigData = gen.generate(cmsData, false);
-
- ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();
-
- /*
- * DEROutputStream is not closeable! OMG!
- */
- DEROutputStream dos = null;
- try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
- dos = new DEROutputStream(outputBytes);
- dos.writeObject(asn1.readObject());
-
- DEROutputStream toClose = dos;
- dos = null;
- toClose.close();
- } catch (IOException e) {
- if (dos != null) {
- try {
- dos.close();
- } catch (IOException ee) {
- e.addSuppressed(ee);
- }
- }
- }
-
- return outputBytes.toByteArray();
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/SigningExtension.java b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
new file mode 100644
index 0000000..2673bed
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
@@ -0,0 +1,392 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apksig.ApkSignerEngine;
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.DefaultApkSignerEngine;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileExtension;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ZFile} extension which signs the APK.
+ *
+ * <p>
+ * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature
+ * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this
+ * extension's constructor.
+ */
+public class SigningExtension {
+ // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
+ // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
+ // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
+ // extension events/callbacks.
+ //
+ // The main issue leading to additional complexity in this class is that the current build
+ // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
+ // matter) for incremental builds. Thus:
+ // * ZFile extension receives no events for JAR entries already in the APK whereas
+ // ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
+ // class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
+ // already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
+ // entries which the incremental build session did not touch.
+ // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
+ // from it whereas ApkSignerEngine produces no output only if it has already produced a signed
+ // APK and no changes have since been made to it. This class addresses this issue by checking
+ // in its "register" method whether the APK is correctly signed and, only if that's the case,
+ // doesn't modify the APK unless a JAR entry is added to it or removed from it after
+ // "register".
+
+ /**
+ * Minimum API Level on which this APK is supposed to run.
+ */
+ private final int mMinSdkVersion;
+
+ /**
+ * Whether JAR signing (aka v1 signing) is enabled.
+ */
+ private final boolean mV1SigningEnabled;
+
+ /**
+ * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled.
+ */
+ private final boolean mV2SigningEnabled;
+
+ /**
+ * Certificate of the signer, to be embedded into the APK's signature.
+ */
+ @Nonnull
+ private final X509Certificate mCertificate;
+
+ /**
+ * APK signer which performs most of the heavy lifting.
+ */
+ @Nonnull
+ private final ApkSignerEngine mSigner;
+
+ /**
+ * Names of APK entries which have been processed by {@link #mSigner}.
+ */
+ private final Set<String> mSignerProcessedOutputEntryNames = new HashSet<>();
+
+ /**
+ * Cached contents of the most recently output APK Signing Block or {@code null} if the block
+ * hasn't yet been output.
+ */
+ @Nullable
+ private byte[] mCachedApkSigningBlock;
+
+ /**
+ * {@code true} if signatures may need to be output, {@code false} if there's no need to output
+ * signatures. This is used in an optimization where we don't modify the APK if it's already
+ * signed and if no JAR entries have been added to or removed from the file.
+ */
+ private boolean mDirty;
+
+ /**
+ * The extension registered with the {@link ZFile}. {@code null} if not registered.
+ */
+ @Nullable
+ private ZFileExtension mExtension;
+
+ /**
+ * The file this extension is attached to. {@code null} if not yet registered.
+ */
+ @Nullable
+ private ZFile mZFile;
+
+ public SigningExtension(
+ int minSdkVersion,
+ @Nonnull X509Certificate certificate,
+ @Nonnull PrivateKey privateKey,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled) throws InvalidKeyException {
+ DefaultApkSignerEngine.SignerConfig signerConfig =
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ "CERT", privateKey, ImmutableList.of(certificate)).build();
+ mSigner =
+ new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion)
+ .setOtherSignersSignaturesPreserved(false)
+ .setV1SigningEnabled(v1SigningEnabled)
+ .setV2SigningEnabled(v2SigningEnabled)
+ .setCreatedBy("1.0 (Android)")
+ .build();
+ mMinSdkVersion = minSdkVersion;
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mCertificate = certificate;
+ }
+
+ public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException {
+ Preconditions.checkState(mExtension == null, "register() already invoked");
+ mZFile = zFile;
+ mDirty = !isCurrentSignatureAsRequested();
+ mExtension = new ZFileExtension() {
+ @Override
+ public IOExceptionRunnable added(
+ @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
+ return () -> onZipEntryOutput(entry);
+ }
+
+ @Override
+ public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ return () -> onZipEntryRemovedFromOutput(entryName);
+ }
+
+ @Override
+ public IOExceptionRunnable beforeUpdate() throws IOException {
+ return () -> onOutputZipReadyForUpdate();
+ }
+
+ @Override
+ public void entriesWritten() throws IOException {
+ onOutputZipEntriesWritten();
+ }
+
+ @Override
+ public void closed() {
+ onOutputClosed();
+ }
+ };
+ mZFile.addZFileExtension(mExtension);
+ }
+
+ /**
+ * Returns {@code true} if the APK's signatures are as requested by parameters to this signing
+ * extension.
+ */
+ private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
+ ApkVerifier.Result result;
+ try {
+ result =
+ new ApkVerifier.Builder(new ZFileDataSource(mZFile))
+ .setMinCheckedPlatformVersion(mMinSdkVersion)
+ .build()
+ .verify();
+ } catch (ApkFormatException e) {
+ // Malformed APK
+ return false;
+ }
+
+ if (!result.isVerified()) {
+ // Signature(s) did not verify
+ return false;
+ }
+
+ if ((result.isVerifiedUsingV1Scheme() != mV1SigningEnabled)
+ || (result.isVerifiedUsingV2Scheme() != mV2SigningEnabled)) {
+ // APK isn't signed with exactly the schemes we want it to be signed
+ return false;
+ }
+
+ List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
+ if (verifiedSignerCerts.size() != 1) {
+ // APK is not signed by exactly one signer
+ return false;
+ }
+
+ byte[] expectedEncodedCert;
+ byte[] actualEncodedCert;
+ try {
+ expectedEncodedCert = mCertificate.getEncoded();
+ actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ // Failed to encode signing certificates
+ return false;
+ }
+
+ if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
+ // APK is signed by a wrong signer
+ return false;
+ }
+
+ // APK is signed the way we want it to be signed
+ return true;
+ }
+
+ private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException {
+ setDirty();
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ // This event may arrive after the entry has already been deleted. In that case, we don't
+ // report the addition of the entry to ApkSignerEngine.
+ if (entry.isDeleted()) {
+ return;
+ }
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ mSigner.outputJarEntry(entryName);
+ mSignerProcessedOutputEntryNames.add(entryName);
+ if (inspectEntryRequest != null) {
+ byte[] entryContents = entry.read();
+ inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length);
+ inspectEntryRequest.done();
+ }
+ }
+
+ private void onZipEntryRemovedFromOutput(@Nonnull String entryName) {
+ setDirty();
+ mSigner.outputJarEntryRemoved(entryName);
+ mSignerProcessedOutputEntryNames.remove(entryName);
+ }
+
+ private void onOutputZipReadyForUpdate() throws IOException {
+ if (!mDirty) {
+ return;
+ }
+
+ // Notify signer engine about ZIP entries that have appeared in the output without the
+ // engine knowing. Also identify ZIP entries which disappeared from the output without the
+ // engine knowing.
+ Set<String> unprocessedRemovedEntryNames = new HashSet<>(mSignerProcessedOutputEntryNames);
+ for (StoredEntry entry : mZFile.entries()) {
+ String entryName = entry.getCentralDirectoryHeader().getName();
+ unprocessedRemovedEntryNames.remove(entryName);
+ if (!mSignerProcessedOutputEntryNames.contains(entryName)) {
+ // Signer engine is not yet aware that this entry is in the output
+ onZipEntryOutput(entry);
+ }
+ }
+
+ // Notify signer engine about entries which disappeared from the output without the engine
+ // knowing
+ for (String entryName : unprocessedRemovedEntryNames) {
+ onZipEntryRemovedFromOutput(entryName);
+ }
+
+ // Check whether we need to output additional JAR entries which comprise the v1 signature
+ ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
+ try {
+ addV1SignatureRequest = mSigner.outputJarEntries();
+ } catch (Exception e) {
+ throw new IOException("Failed to generate v1 signature", e);
+ }
+ if (addV1SignatureRequest == null) {
+ return;
+ }
+
+ // We need to output additional JAR entries which comprise the v1 signature
+ List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
+ new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());
+
+ // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
+ // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
+ // ManifestGenerationExtension.
+ for (int i = 0; i < v1SignatureEntries.size(); i++) {
+ ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
+ String name = entry.getName();
+ if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
+ continue;
+ }
+ if (i != 0) {
+ v1SignatureEntries.remove(i);
+ v1SignatureEntries.add(0, entry);
+ }
+ break;
+ }
+
+ // Output the JAR entries comprising the v1 signature
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
+ String name = entry.getName();
+ byte[] data = entry.getData();
+ mZFile.add(name, new ByteArrayInputStream(data));
+ }
+
+ addV1SignatureRequest.done();
+ }
+
+ private void onOutputZipEntriesWritten() throws IOException {
+ if (!mDirty) {
+ return;
+ }
+
+ // Check whether we should output an APK Signing Block which contains v2 signatures
+ byte[] apkSigningBlock;
+ byte[] centralDirBytes = mZFile.getCentralDirectoryBytes();
+ byte[] eocdBytes = mZFile.getEocdBytes();
+ ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest;
+ // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
+ // cache the block to speed things up. The cached block is invalidated by any changes to the
+ // file (as reported to this extension).
+ if (mCachedApkSigningBlock != null) {
+ apkSigningBlock = mCachedApkSigningBlock;
+ addV2SignatureRequest = null;
+ } else {
+ DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
+ DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
+ long zipEntriesSizeBytes =
+ mZFile.getCentralDirectoryOffset() - mZFile.getExtraDirectoryOffset();
+ DataSource zipEntries = new ZFileDataSource(mZFile, 0, zipEntriesSizeBytes);
+ try {
+ addV2SignatureRequest = mSigner.outputZipSections(zipEntries, centralDir, eocd);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException
+ | ApkFormatException | IOException e) {
+ throw new IOException("Failed to generate v2 signature", e);
+ }
+ apkSigningBlock =
+ (addV2SignatureRequest != null)
+ ? addV2SignatureRequest.getApkSigningBlock() : new byte[0];
+ mCachedApkSigningBlock = apkSigningBlock;
+ }
+
+ // Insert the APK Signing Block into the output right before the ZIP Central Directory and
+ // accordingly update the start offset of ZIP Central Directory in ZIP End of Central
+ // Directory.
+ mZFile.directWrite(
+ mZFile.getCentralDirectoryOffset() - mZFile.getExtraDirectoryOffset(),
+ apkSigningBlock);
+ mZFile.setExtraDirectoryOffset(apkSigningBlock.length);
+
+ if (addV2SignatureRequest != null) {
+ addV2SignatureRequest.done();
+ }
+ }
+
+ private void onOutputClosed() {
+ if (!mDirty) {
+ return;
+ }
+ mSigner.outputDone();
+ mDirty = false;
+ }
+
+ private void setDirty() {
+ mDirty = true;
+ mCachedApkSigningBlock = null;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
new file mode 100644
index 0000000..c42db12
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
@@ -0,0 +1,157 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apkzlib.zip.ZFile;
+import com.google.common.base.Preconditions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
+
+/**
+ * {@link DataSource} backed by contents of {@link ZFile}.
+ */
+class ZFileDataSource implements DataSource {
+
+ private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+ @Nonnull
+ private final ZFile mFile;
+
+ /**
+ * Offset (in bytes) relative to the start of file where the region visible in this data source
+ * starts.
+ */
+ private final long mOffset;
+
+ /**
+ * Size (in bytes) of the file region visible in this data source or {@code -1} if the whole
+ * file is visible in this data source and thus its size may change if the file's size changes.
+ */
+ private final long mSize;
+
+ /**
+ * Constructs a new {@code ZFileDataSource} based on the data contained in the file. Changes to
+ * the contents of the file, including the size of the file, will be visible in this data
+ * source.
+ */
+ public ZFileDataSource(@Nonnull ZFile file) {
+ mFile = file;
+ mOffset = 0;
+ mSize = -1;
+ }
+
+ /**
+ * Constructs a new {@code ZFileDataSource} based on the data contained in the specified region
+ * of the provided file. Changes to the contents of this region of the file will be visible in
+ * this data source.
+ */
+ public ZFileDataSource(@Nonnull ZFile file, long offset, long size) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(size >= 0, "size < 0");
+ mFile = file;
+ mOffset = offset;
+ mSize = size;
+ }
+
+ @Override
+ public long size() {
+ if (mSize == -1) {
+ // Data source size is the current size of the file
+ try {
+ return mFile.directSize();
+ } catch (IOException e) {
+ return 0;
+ }
+ } else {
+ // Data source size is fixed
+ return mSize;
+ }
+ }
+
+ @Override
+ public DataSource slice(long offset, long size) {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if ((offset == 0) && (size == sourceSize)) {
+ return this;
+ }
+
+ return new ZFileDataSource(mFile, mOffset + offset, size);
+ }
+
+ @Override
+ public void feed(long offset, long size, @Nonnull DataSink sink) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ long chunkOffsetInFile = mOffset + offset;
+ long remaining = size;
+ byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
+ while (remaining > 0) {
+ int chunkSize = (int) Math.min(remaining, buf.length);
+ int readSize = mFile.directRead(chunkOffsetInFile, buf, 0, chunkSize);
+ if (readSize == -1) {
+ throw new EOFException("Premature EOF");
+ }
+ if (readSize > 0) {
+ sink.consume(buf, 0, readSize);
+ chunkOffsetInFile += readSize;
+ remaining -= readSize;
+ }
+ }
+ }
+
+ @Override
+ public void copyTo(long offset, int size, @Nonnull ByteBuffer dest) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ int prevLimit = dest.limit();
+ try {
+ mFile.directFullyRead(mOffset + offset, dest);
+ } finally {
+ dest.limit(prevLimit);
+ }
+ }
+
+ @Override
+ public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+ ByteBuffer result = ByteBuffer.allocate(size);
+ copyTo(offset, size, result);
+ result.flip();
+ return result;
+ }
+
+ private static void checkChunkValid(long offset, long size, long sourceSize) {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+ Preconditions.checkArgument(size >= 0, "size < 0");
+ Preconditions.checkArgument(offset <= sourceSize, "offset > sourceSize");
+ long endOffset = offset + size;
+ Preconditions.checkArgument(offset <= endOffset, "offset > endOffset");
+ Preconditions.checkArgument(endOffset <= sourceSize, "endOffset > sourceSize");
+ }
+}
diff --git a/src/main/java/com/android/apkzlib/sign/package-info.java b/src/main/java/com/android/apkzlib/sign/package-info.java
index bdcb02e..6bb692c 100644
--- a/src/main/java/com/android/apkzlib/sign/package-info.java
+++ b/src/main/java/com/android/apkzlib/sign/package-info.java
@@ -30,7 +30,7 @@ and will change the zip file itself.
<p>
The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will
ensure the zip has a manifest file and is, therefore, a valid jar.
-The {@link com.android.apkzlib.sign.SignatureExtension} extension will
+The {@link com.android.apkzlib.sign.SigningExtension} extension will
ensure the jar is signed.
<p>
The extension mechanism used is the one provided in the {@code zip} package (see
@@ -72,7 +72,7 @@ follows (if only the manifest generation extension was added to the {@code ZFile
<li>The zip is finally written with an updated manifest.</li>
</ol>
<p>
-To generate a signed apk (v1), we need to add a second extension, the {@code SignatureExtension}.
+To generate a signed apk, we need to add a second extension, the {@code SigningExtension}.
This extension will also register listeners with the {@code ZFile}.
<p>
In this case the flow would be (starting a bit earlier for clarity and assuming a package task
@@ -85,9 +85,9 @@ in the build process):
<li>Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.</li>
<li>The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid
manifest. No changes are done to the {@code ZFile}.</li>
- <li>Package task creates a {@code SignatureExtension}.</li>
- <li>Package task registers the {@code SignatureExtension} with the {@code ZFile}.</li>
- <li>The {@code SignatureExtension} registers a {@code ZFileExtension} with the {@code ZFile}
+ <li>Package task creates a {@code SigningExtension}.</li>
+ <li>Package task registers the {@code SigningExtension} with the {@code ZFile}.</li>
+ <li>The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile}
and look at the {@code ZFile} to see if there is a valid signature file.</li>
<li>If there are changes to the digital signature file needed, these are marked internally in
the extension. If there are changes needed to the digests, the manifest is updated (by calling
@@ -100,7 +100,7 @@ in the build process):
<li>For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added}
method of all registered extensions.</li>
<li>The {@code ManifestGenerationExtension} ignores added invocations.</li>
- <li>The {@code SignatureExtension} computes the digest for the added file and stores them in
+ <li>The {@code SigningExtension} computes the digest for the added file and stores them in
the manifest.<br>
<em>(when all files are added to the apk, all digests are computed and the manifest is updated
but only in memory; the apk file has not been touched; also note that {@code ZFile} has not
@@ -108,15 +108,15 @@ in the build process):
<li>Package task calls {@code ZFile.update()} to update the apk.</li>
<li>{@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is
done before anything is written. In this case both the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} are invoked.</li>
+ {@code SigningExtension} are invoked.</li>
<li>The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new manifest,
unless nothing has changed, in which case it does nothing.</li>
- <li>The {@code SignatureExtension} will add the SF file (unless nothing has changed), will
+ <li>The {@code SigningExtension} will add the SF file (unless nothing has changed), will
compute the digital signature of the SF file and write it to the {@code ZFile}.<br>
<em>(note that the order by which the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} are called is non-deterministic; however, this is not a problem
+ {@code SigningExtension} are called is non-deterministic; however, this is not a problem
because the manifest is already computed by the {@code ManifestGenerationExtension} at this
- time and the {@code SignatureExtension} will obtain the manifest data from the
+ time and the {@code SigningExtension} will obtain the manifest data from the
{@code ManifestGenerationExtension} and not from the {@code ZFile}; this means that the
{@code SF} file may be added to the {@code ZFile} before the {@code MF} file, but that is
irrelevant.)</em></li>
@@ -124,9 +124,8 @@ in the build process):
{@code ZFile.update()} method continues.</li>
<li>{@code ZFile.update()} writes all changes and new entries to the zip file.</li>
<li>{@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all
- registered extensions. Both the {@code ManifestGenerationExtension} and
- {@code SignatureExtension} ignore this notification -- but the {@code FullApkSignExtension} will
- kick in at this point, if it has been created.</li>
+ registered extensions. {@code SigningExtension} will kick in at this point, if v2 signature
+ has changed.</li>
<li>{@code ZFile} writes the central directory and EOCD.</li>
<li>{@code ZFile.update()} returns control to the package task.</li>
<li>The package task finishes.</li>
@@ -139,17 +138,15 @@ zip).</em>
<p>
If there are no changes to the {@code ZFile} made by the package task and the file's manifest and v1
signatures are correct, neither the {@code ManifestGenerationExtension} nor the
-{@code SignatureExtension} will not do anything on the {@code beforeUpdate()} and the
+{@code SigningExtension} will not do anything on the {@code beforeUpdate()} and the
{@code ZFile} won't even be open for writing.
<p>
This implementation provides perfect incremental updates.
<p>
Additionally, by adding/removing extensions we can configure what type of apk we want:
<ul>
- <li>No SignatureExtension &amp; No FullApkSignExtension ⇒ Aligned, unsigned apk.</li>
- <li>Signature Extension &amp; No FullApkSignExtension ⇒ Aligned, v1 only signed apk.</li>
- <li>Signature Extension &amp; FullApkSignExtension ⇒ Aligned, v1 &amp; v2 signed apk.</li>
- <li>No Signature Extension &amp; FullApkSignExtension ⇒ Aligned, v2 only signed apk.</li>
+ <li>No SigningExtension ⇒ Aligned, unsigned apk.</li>
+ <li>SigningExtension ⇒ Aligned, signed apk.
</ul>
So, by configuring which extensions to add, the package task can decide what type of apk we want.
*/
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java b/src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java
deleted file mode 100644
index 71cf026..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ApkSignerV2.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/*
- * 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.apkzlib.sign.v2;
-
-import com.android.apkzlib.utils.ApkZLibPair;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.security.DigestException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.security.interfaces.ECKey;
-import java.security.interfaces.RSAKey;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.Nonnull;
-
-/**
- * APK Signature Scheme v2 signer.
- *
- * <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 ApkSignerV2 {
- /*
- * The two main goals of APK Signature Scheme v2 are:
- * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
- * cover every byte of the APK being signed.
- * 2. Enable much faster signature and integrity verification. This is achieved by requiring
- * only a minimal amount of APK parsing before the signature is verified, thus completely
- * bypassing ZIP entry decompression and by making integrity verification parallelizable by
- * employing a hash tree.
- *
- * The generated signature block is wrapped into an APK Signing Block and inserted into the
- * original APK immediately before the start of ZIP Central Directory. This is to ensure that
- * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
- * extensibility. For example, a future signature scheme could insert its signatures there as
- * well. The contract of the APK Signing Block is that all contents outside of the block must be
- * protected by signatures inside the block.
- */
-
- private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
-
- private static final byte[] APK_SIGNING_BLOCK_MAGIC =
- new byte[] {
- 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
- 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
- };
- private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
- private ApkSignerV2() {}
-
- /**
- * Signer configuration.
- */
- public static final class SignerConfig {
- /**
- * Private key.
- */
- @Nonnull
- public PrivateKey privateKey;
-
- /**
- * Certificates, with the first certificate containing the public key corresponding to
- * {@link #privateKey}.
- */
- @Nonnull
- public List<X509Certificate> certificates;
-
- /**
- * List of signature algorithms with which to sign. At least one algorithm must be
- * provided.
- */
- @Nonnull
- public List<SignatureAlgorithm> signatureAlgorithms;
- }
-
- /**
- * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
- * provided key.
- *
- * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
- * AndroidManifest.xml minSdkVersion attribute)
- *
- * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
- * APK Signature Scheme v2
- */
- public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
- @Nonnull PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
- String keyAlgorithm = signingKey.getAlgorithm();
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
- // deterministic signatures which make life easier for OTA updates (fewer files
- // changed when deterministic signature schemes are used).
-
- // Pick a digest which is no weaker than the key.
- int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
- if (modulusLengthBits <= 3072) {
- // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
- return ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
- } else {
- // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
- // digest being the weak link. SHA-512 is the next strongest supported digest.
- return ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
- }
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- // DSA is supported only with SHA-256.
- return ImmutableList.of(SignatureAlgorithm.DSA_WITH_SHA256);
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- // Pick a digest which is no weaker than the key.
- int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
- if (keySizeBits <= 256) {
- // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
- return ImmutableList.of(SignatureAlgorithm.ECDSA_WITH_SHA256);
- } else {
- // Keys longer than 256 bit need to be paired with a stronger digest to avoid the
- // digest being the weak link. SHA-512 is the next strongest supported digest.
- return ImmutableList.of(SignatureAlgorithm.ECDSA_WITH_SHA512);
- }
- } else {
- throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
- }
- }
-
- /**
- * Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block
- * containing the signature.
- *
- * <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
- * of META-INF/*.SF files of APK being signed must contain the
- * {@code X-Android-APK-Signed: 2} attribute.
- *
- * @param signerConfigs signer configurations, one for each signer. At least one configuration
- * must be provided.
- *
- * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
- * cannot be used in general
- * @throws SignatureException if an error occurs when computing digests of generating
- * signatures
- */
- @Nonnull
- public static byte[] generateApkSigningBlock(
- @Nonnull DigestSource beforeCentralDir,
- @Nonnull DigestSource centralDir,
- @Nonnull DigestSource eocd,
- @Nonnull List<SignerConfig> signerConfigs)
- throws InvalidKeyException, SignatureException {
- if (signerConfigs.isEmpty()) {
- throw new IllegalArgumentException(
- "No signer configs provided. At least one is required");
- }
-
- // Figure out which digest(s) to use for APK contents.
- Set<ContentDigestAlgorithm> contentDigestAlgorithms = Sets.newHashSetWithExpectedSize(1);
- for (SignerConfig signerConfig : signerConfigs) {
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
- }
- }
-
- // Compute digests of APK contents.
- Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
- try {
- contentDigests =
- computeContentDigests(
- contentDigestAlgorithms,
- new DigestSource[] {beforeCentralDir, centralDir, eocd});
- } catch (DigestException e) {
- throw new SignatureException("Failed to compute digests of APK", e);
- }
-
- // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
- return generateApkSigningBlock(signerConfigs, contentDigests);
- }
-
- @Nonnull
- private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
- @Nonnull Set<ContentDigestAlgorithm> digestAlgorithms,
- @Nonnull DigestSource[] contents) throws DigestException {
- // For each digest algorithm the result is computed as follows:
- // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
- // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
- // No chunks are produced for empty (zero length) segments.
- // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
- // length in bytes (uint32 little-endian) and the chunk's contents.
- // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
- // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
- // segments in-order.
-
- long chunkCountLong = 0;
- for (DigestSource input : contents) {
- chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- }
- if (chunkCountLong >= Integer.MAX_VALUE) {
- throw new DigestException("Input too long: " + chunkCountLong + " chunks");
- }
- int chunkCount = (int) chunkCountLong;
-
- ContentDigestAlgorithm[] digestAlgorithmsArray =
- digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
- MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
- byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
- int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
- for (int i = 0; i < digestAlgorithmsArray.length; i++) {
- ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
- int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
- digestOutputSizes[i] = digestOutputSizeBytes;
- byte[] concatenationOfChunkCountAndChunkDigests =
- new byte[5 + chunkCount * digestOutputSizeBytes];
- concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
- setUnsignedInt32LittleEndian(
- chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
- digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
- String jcaAlgorithmName = digestAlgorithm.getJcaMessageDigestAlgorithmName();
- try {
- mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(jcaAlgorithmName + " MessageDigest not supported", e);
- }
- }
-
- byte[] chunkContentPrefix = new byte[5];
- chunkContentPrefix[0] = (byte) 0xa5;
- int chunkIndex = 0;
- // Optimization opportunity: digests of chunks can be computed in parallel. However,
- // determining the number of computations to be performed in parallel is non-trivial. This
- // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
- // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
- // cores, load on the system from other threads of execution and other processes, size of
- // input.
- // For now, we compute these digests sequentially and thus have the luxury of improving
- // performance by writing the digest of each chunk into a pre-allocated buffer at exactly
- // the right position. This avoids unnecessary allocations, copying, and enables the final
- // digest to be more efficient because it's presented with all of its input in one go.
- for (DigestSource input : contents) {
- long inputOffset = 0;
- long inputRemaining = input.size();
- while (inputRemaining > 0) {
- int chunkSize =
- (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
- for (int i = 0; i < mds.length; i++) {
- mds[i].update(chunkContentPrefix);
- }
- try {
- input.feedDigests(inputOffset, chunkSize, mds);
- } catch (IOException e) {
- throw new DigestException("Failed to digest chunk #" + chunkIndex, e);
- }
- for (int i = 0; i < digestAlgorithmsArray.length; i++) {
- MessageDigest md = mds[i];
- byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
- int expectedDigestSizeBytes = digestOutputSizes[i];
- int actualDigestSizeBytes =
- md.digest(
- concatenationOfChunkCountAndChunkDigests,
- 5 + chunkIndex * expectedDigestSizeBytes,
- expectedDigestSizeBytes);
- if (actualDigestSizeBytes != expectedDigestSizeBytes) {
- throw new RuntimeException(
- "Unexpected output size of " + md.getAlgorithm()
- + " digest: " + actualDigestSizeBytes);
- }
- }
- inputOffset += chunkSize;
- inputRemaining -= chunkSize;
- chunkIndex++;
- }
- }
-
- Map<ContentDigestAlgorithm, byte[]> result =
- Maps.newHashMapWithExpectedSize(digestAlgorithmsArray.length);
- for (int i = 0; i < digestAlgorithmsArray.length; i++) {
- ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
- byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
- MessageDigest md = mds[i];
- byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
- result.put(digestAlgorithm, digest);
- }
- return result;
- }
-
- private static final long getChunkCount(long inputSize, int chunkSize) {
- return (inputSize + chunkSize - 1) / chunkSize;
- }
-
- private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
- result[offset] = (byte) (value & 0xff);
- result[offset + 1] = (byte) ((value >> 8) & 0xff);
- result[offset + 2] = (byte) ((value >> 16) & 0xff);
- result[offset + 3] = (byte) ((value >> 24) & 0xff);
- }
-
- private static byte[] generateApkSigningBlock(
- List<SignerConfig> signerConfigs,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws InvalidKeyException, SignatureException {
- byte[] apkSignatureSchemeV2Block =
- generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
- return generateApkSigningBlock(apkSignatureSchemeV2Block);
- }
-
- private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
- // FORMAT:
- // uint64: size (excluding this field)
- // repeated ID-value pairs:
- // uint64: size (excluding this field)
- // uint32: ID
- // (size - 4) bytes: value
- // uint64: size (same as the one above)
- // uint128: magic
-
- int resultSize =
- 8 // size
- + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
- + 8 // size
- + 16 // magic
- ;
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- long blockSizeFieldValue = resultSize - 8;
- result.putLong(blockSizeFieldValue);
-
- long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
- result.putLong(pairSizeFieldValue);
- result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
- result.put(apkSignatureSchemeV2Block);
-
- result.putLong(blockSizeFieldValue);
- result.put(APK_SIGNING_BLOCK_MAGIC);
-
- return result.array();
- }
-
- private static byte[] generateApkSignatureSchemeV2Block(
- List<SignerConfig> signerConfigs,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws InvalidKeyException, SignatureException {
- // FORMAT:
- // * length-prefixed sequence of length-prefixed signer blocks.
-
- List<byte[]> signerBlocks = Lists.newArrayListWithExpectedSize(signerConfigs.size());
- int signerNumber = 0;
- for (SignerConfig signerConfig : signerConfigs) {
- signerNumber++;
- byte[] signerBlock;
- try {
- signerBlock = generateSignerBlock(signerConfig, contentDigests);
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
- } catch (SignatureException e) {
- throw new SignatureException("Signer #" + signerNumber + " failed", e);
- }
- signerBlocks.add(signerBlock);
- }
-
- return encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
- });
- }
-
- private static byte[] generateSignerBlock(
- SignerConfig signerConfig,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws InvalidKeyException, SignatureException {
- if (signerConfig.certificates.isEmpty()) {
- throw new SignatureException("No certificates configured for signer");
- }
- PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
- byte[] encodedPublicKey = encodePublicKey(publicKey);
-
- V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
- try {
- signedData.certificates = encodeCertificates(signerConfig.certificates);
- } catch (CertificateEncodingException e) {
- throw new SignatureException("Failed to encode certificates", e);
- }
-
- List<ApkZLibPair<Integer, byte[]>> digests =
- Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- ContentDigestAlgorithm contentDigestAlgorithm =
- signatureAlgorithm.getContentDigestAlgorithm();
- byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
- if (contentDigest == null) {
- throw new RuntimeException(
- contentDigestAlgorithm + " content digest for " + signatureAlgorithm
- + " not computed");
- }
- digests.add(new ApkZLibPair<>(signatureAlgorithm.getId(), contentDigest));
- }
- signedData.digests = digests;
-
- V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
- // FORMAT:
- // * length-prefixed sequence of length-prefixed digests:
- // * uint32: signature algorithm ID
- // * length-prefixed bytes: digest of contents
- // * length-prefixed sequence of certificates:
- // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
- // * length-prefixed sequence of length-prefixed additional attributes:
- // * uint32: ID
- // * (length - 4) bytes: value
- signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
- encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
- // additional attributes
- new byte[0],
- });
- signer.publicKey = encodedPublicKey;
- signer.signatures =
- Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
- for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
- signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
- String jcaSignatureAlgorithm = sigAlgAndParams.v1;
- AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.v2;
- byte[] signatureBytes;
- try {
- Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
- signature.initSign(signerConfig.privateKey);
- if (jcaSignatureAlgorithmParams != null) {
- signature.setParameter(jcaSignatureAlgorithmParams);
- }
- signature.update(signer.signedData);
- signatureBytes = signature.sign();
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
- | SignatureException e) {
- throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
- }
-
- try {
- Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
- signature.initVerify(publicKey);
- if (jcaSignatureAlgorithmParams != null) {
- signature.setParameter(jcaSignatureAlgorithmParams);
- }
- signature.update(signer.signedData);
- if (!signature.verify(signatureBytes)) {
- throw new SignatureException("Signature did not verify");
- }
- } catch (InvalidKeyException e) {
- throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
- + " signature using public key from certificate", e);
- } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
- | SignatureException e) {
- throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
- + " signature using public key from certificate", e);
- }
-
- signer.signatures.add(new ApkZLibPair<>(signatureAlgorithm.getId(), signatureBytes));
- }
-
- // FORMAT:
- // * length-prefixed signed data
- // * length-prefixed sequence of length-prefixed signatures:
- // * uint32: signature algorithm ID
- // * length-prefixed bytes: signature of signed data
- // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
- return encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- signer.signedData,
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
- signer.signatures),
- signer.publicKey,
- });
- }
-
- private static final class V2SignatureSchemeBlock {
- private static final class Signer {
- public byte[] signedData;
- public List<ApkZLibPair<Integer, byte[]>> signatures;
- public byte[] publicKey;
- }
-
- private static final class SignedData {
- public List<ApkZLibPair<Integer, byte[]>> digests;
- public List<byte[]> certificates;
- }
- }
-
- private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
- byte[] encodedPublicKey = null;
- if ("X.509".equals(publicKey.getFormat())) {
- encodedPublicKey = publicKey.getEncoded();
- }
- if (encodedPublicKey == null) {
- try {
- encodedPublicKey =
- KeyFactory.getInstance(publicKey.getAlgorithm())
- .getKeySpec(publicKey, X509EncodedKeySpec.class)
- .getEncoded();
- } catch (NoSuchAlgorithmException e) {
- throw new InvalidKeyException(
- "Failed to obtain X.509 encoded form of public key " + publicKey
- + " of class " + publicKey.getClass().getName(),
- e);
- } catch (InvalidKeySpecException e) {
- throw new InvalidKeyException(
- "Failed to obtain X.509 encoded form of public key " + publicKey
- + " of class " + publicKey.getClass().getName(),
- e);
- }
- }
- if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
- throw new InvalidKeyException(
- "Failed to obtain X.509 encoded form of public key " + publicKey
- + " of class " + publicKey.getClass().getName());
- }
- return encodedPublicKey;
- }
-
- private static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
- throws CertificateEncodingException {
- List<byte[]> result = Lists.newArrayListWithExpectedSize(certificates.size());
- for (X509Certificate certificate : certificates) {
- result.add(certificate.getEncoded());
- }
- return result;
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
- return encodeAsSequenceOfLengthPrefixedElements(
- sequence.toArray(new byte[sequence.size()][]));
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
- int payloadSize = 0;
- for (byte[] element : sequence) {
- payloadSize += 4 + element.length;
- }
- ByteBuffer result = ByteBuffer.allocate(payloadSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (byte[] element : sequence) {
- result.putInt(element.length);
- result.put(element);
- }
- return result.array();
- }
-
- private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
- List<ApkZLibPair<Integer, byte[]>> sequence) {
- int resultSize = 0;
- for (ApkZLibPair<Integer, byte[]> element : sequence) {
- resultSize += 12 + element.v2.length;
- }
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (ApkZLibPair<Integer, byte[]> element : sequence) {
- byte[] second = element.v2;
- result.putInt(8 + second.length);
- result.putInt(element.v1);
- result.putInt(second.length);
- result.put(second);
- }
- return result.array();
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java b/src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java
deleted file mode 100644
index cd2ed1d..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ByteArrayDigestSource.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.apkzlib.sign.v2;
-
-import com.google.common.base.Preconditions;
-import java.security.MessageDigest;
-import javax.annotation.Nonnull;
-
-/**
- * {@code byte[]} which is fed into {@link MessageDigest} instances.
- */
-public class ByteArrayDigestSource implements DigestSource {
- private final byte[] mBuf;
-
- /**
- * Constructs a new {@code ByteArrayDigestSource} instance which obtains its data from the
- * provided byte array. Changes to the byte array's contents are reflected visible in this
- * source.
- */
- public ByteArrayDigestSource(@Nonnull byte[] buf) {
- mBuf = buf;
- }
-
- @Override
- public long size() {
- return mBuf.length;
- }
-
- @Override
- public void feedDigests(long offset, int size, @Nonnull MessageDigest[] digests) {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- Preconditions.checkArgument(offset <= mBuf.length, "offset too large: %s", offset);
- int offsetInBuf = (int) offset;
- Preconditions.checkPositionIndex(offsetInBuf, mBuf.length, "offset out of range");
- int availableSize = mBuf.length - offsetInBuf;
- Preconditions.checkArgument(
- size <= availableSize,
- "offset (%s) + size (%s) > array length (%s)", offset, size, mBuf.length);
-
- for (MessageDigest md : digests) {
- md.update(mBuf, offsetInBuf, size);
- }
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java
deleted file mode 100644
index 46678e4..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ContentDigestAlgorithm.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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.apkzlib.sign.v2;
-
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
-enum ContentDigestAlgorithm {
- /** SHA2-256 over 1 MB chunks. */
- CHUNKED_SHA256("SHA-256", 256 / 8),
-
- /** SHA2-512 over 1 MB chunks. */
- CHUNKED_SHA512("SHA-512", 512 / 8);
-
- private final String mJcaMessageDigestAlgorithmName;
- private final int mChunkDigestOutputSizeBytes;
-
- private ContentDigestAlgorithm(
- String jcaMessageDigestAlgorithmName, int chunkDigestOutputSizeBytes) {
- mJcaMessageDigestAlgorithmName = jcaMessageDigestAlgorithmName;
- mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
- }
-
- /**
- * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
- * chunks by this content digest algorithm.
- */
- String getJcaMessageDigestAlgorithmName() {
- return mJcaMessageDigestAlgorithmName;
- }
-
- /**
- * Returns the size (in bytes) of the digest of a chunk of content.
- */
- int getChunkDigestOutputSizeBytes() {
- return mChunkDigestOutputSizeBytes;
- }
-} \ No newline at end of file
diff --git a/src/main/java/com/android/apkzlib/sign/v2/DigestSource.java b/src/main/java/com/android/apkzlib/sign/v2/DigestSource.java
deleted file mode 100644
index 4a9cc19..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/DigestSource.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.apkzlib.sign.v2;
-
-import java.io.IOException;
-import java.security.MessageDigest;
-import javax.annotation.Nonnull;
-
-/**
- * Source of data which is fed into {@link MessageDigest} instances.
- *
- * <p>This abstraction serves two purposes:
- * <ul>
- * <li>Transparent digesting of different types of sources, such as {@code byte[]},
- * {@code ZFile}, {@link java.nio.ByteBuffer} and/or memory-mapped file.</li>
- * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
- * would have worked as the unifying abstraction.</li>
- * </ul>
- */
-public interface DigestSource {
- /**
- * Returns the amount of data (in bytes) contained in this data source.
- */
- long size();
-
- /**
- * Feeds the specified chunk from this data source into each of the provided
- * {@link MessageDigest} instances. Each {@code MessageDigest} instance receives the specified
- * chunk of data in full.
- *
- * @param offset index (in bytes) at which the chunk starts relative to the start of this data
- * source.
- * @param size size (in bytes) of the chunk.
- */
- void feedDigests(long offset, int size, @Nonnull MessageDigest[] digests) throws IOException;
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java b/src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java
deleted file mode 100644
index b922d8a..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/SignatureAlgorithm.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.apkzlib.sign.v2;
-
-import com.android.apkzlib.utils.ApkZLibPair;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.MGF1ParameterSpec;
-import java.security.spec.PSSParameterSpec;
-
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
-public enum SignatureAlgorithm {
- /**
- * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
- * digested using SHA2-256 in 1 MB chunks.
- */
- RSA_PSS_WITH_SHA256(
- 0x0101,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withRSA/PSS",
- new PSSParameterSpec(
- "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))),
-
- /**
- * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
- * digested using SHA2-512 in 1 MB chunks.
- */
- RSA_PSS_WITH_SHA512(
- 0x0102,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>(
- "SHA512withRSA/PSS",
- new PSSParameterSpec(
- "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))),
-
- /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- RSA_PKCS1_V1_5_WITH_SHA256(
- 0x0103,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withRSA", null)),
-
- /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
- RSA_PKCS1_V1_5_WITH_SHA512(
- 0x0104,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>("SHA512withRSA", null)),
-
- /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- ECDSA_WITH_SHA256(
- 0x0201,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withECDSA", null)),
-
- /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
- ECDSA_WITH_SHA512(
- 0x0202,
- ContentDigestAlgorithm.CHUNKED_SHA512,
- new ApkZLibPair<>("SHA512withECDSA", null)),
-
- /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
- DSA_WITH_SHA256(
- 0x0301,
- ContentDigestAlgorithm.CHUNKED_SHA256,
- new ApkZLibPair<>("SHA256withDSA", null));
-
- private final int mId;
- private final ContentDigestAlgorithm mContentDigestAlgorithm;
- private final ApkZLibPair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
-
- private SignatureAlgorithm(int id,
- ContentDigestAlgorithm contentDigestAlgorithm,
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
- mId = id;
- mContentDigestAlgorithm = contentDigestAlgorithm;
- mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
- }
-
- /**
- * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
- */
- int getId() {
- return mId;
- }
-
- /**
- * Returns the content digest algorithm associated with this signature algorithm.
- */
- ContentDigestAlgorithm getContentDigestAlgorithm() {
- return mContentDigestAlgorithm;
- }
-
- /**
- * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
- * (or null if not needed) to parameterize the {@code Signature}.
- */
- ApkZLibPair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
- return mJcaSignatureAlgAndParams;
- }
-}
diff --git a/src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java b/src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java
deleted file mode 100644
index 31ae812..0000000
--- a/src/main/java/com/android/apkzlib/sign/v2/ZFileDigestSource.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.apkzlib.sign.v2;
-
-import com.android.apkzlib.zip.ZFile;
-import com.google.common.base.Preconditions;
-import java.io.IOException;
-import java.security.MessageDigest;
-import javax.annotation.Nonnull;
-
-/**
- * Contiguous section of {@link ZFile} which is fed into {@link MessageDigest} instances.
- */
-public class ZFileDigestSource implements DigestSource {
- private final ZFile mFile;
- private final long mOffset;
- private final long mSize;
-
- /**
- * Constructs a new {@code ZFileDigestSource} representing the section of the file starting
- * at the provided {@code offset} and extending for the provided {@code size} number of bytes.
- */
- public ZFileDigestSource(@Nonnull ZFile file, long offset, long size) {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- mFile = file;
- mOffset = offset;
- mSize = size;
- }
-
-
- @Override
- public long size() {
- return mSize;
- }
-
- @Override
- public void feedDigests(long offset, int size, @Nonnull MessageDigest[] digests)
- throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
- Preconditions.checkArgument(size >= 0, "size: %s", size);
- Preconditions.checkArgument(offset <= mSize, "offset: %s, file size: %s", offset, mSize);
- long chunkStartOffset = mOffset + offset;
- long availableSize = mSize - offset;
- Preconditions.checkArgument(
- size <= availableSize, "offset: %s, size: %s, file size: %s", offset, size, mSize);
-
- byte[] chunk = new byte[size];
- mFile.directFullyRead(chunkStartOffset, chunk);
- for (MessageDigest md : digests) {
- md.update(chunk);
- }
- }
-}
diff --git a/src/main/java/com/android/apkzlib/zfile/ZFiles.java b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
index fdf6983..d5102a6 100644
--- a/src/main/java/com/android/apkzlib/zfile/ZFiles.java
+++ b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
@@ -16,12 +16,10 @@
package com.android.apkzlib.zfile;
-import com.android.apkzlib.sign.FullApkSignExtension;
import com.android.apkzlib.sign.ManifestGenerationExtension;
-import com.android.apkzlib.sign.SignatureExtension;
+import com.android.apkzlib.sign.SigningExtension;
import com.android.apkzlib.zip.AlignmentRule;
import com.android.apkzlib.zip.AlignmentRules;
-import com.android.apkzlib.zip.StoredEntry;
import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileOptions;
import java.io.File;
@@ -118,35 +116,12 @@ public class ZFiles {
if (key != null && certificate != null) {
try {
- if (v1SigningEnabled) {
- String apkSignedHeaderValue =
- (v2SigningEnabled)
- ? SignatureExtension
- .SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED
- : null;
- SignatureExtension jarSignatureSchemeExt = new SignatureExtension(manifestExt,
- minSdkVersion, certificate, key,
- apkSignedHeaderValue);
- jarSignatureSchemeExt.register();
- }
- if (v2SigningEnabled) {
- FullApkSignExtension apkSignatureSchemeV2Ext =
- new FullApkSignExtension(
- zfile,
- minSdkVersion,
- certificate,
- key);
- apkSignatureSchemeV2Ext.register();
- }
- if (!v1SigningEnabled) {
- // Remove v1 signature files from the APK
- for (StoredEntry entry : zfile.entries()) {
- if (SignatureExtension.isIgnoredFile(
- entry.getCentralDirectoryHeader().getName())) {
- entry.delete();
- }
- }
- }
+ new SigningExtension(
+ minSdkVersion,
+ certificate,
+ key,
+ v1SigningEnabled,
+ v2SigningEnabled).register(zfile);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IOException("Failed to create signature extensions", e);
}
diff --git a/src/main/java/com/android/apkzlib/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
index 83b9ca3..a8ee56e 100644
--- a/src/main/java/com/android/apkzlib/zip/StoredEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
@@ -365,6 +365,13 @@ public class StoredEntry {
}
/**
+ * Returns {@code true} if this entry has been deleted/replaced.
+ */
+ public boolean isDeleted() {
+ return mDeleted;
+ }
+
+ /**
* Obtains the CDH associated with this entry.
*
* @return the CDH
diff --git a/src/main/java/com/android/apkzlib/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java
index e8798a5..301b942 100644
--- a/src/main/java/com/android/apkzlib/zip/ZFile.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFile.java
@@ -22,7 +22,6 @@ import com.android.apkzlib.utils.IOExceptionRunnable;
import com.android.apkzlib.zip.utils.ByteTracker;
import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.android.apkzlib.zip.utils.LittleEndianUtils;
-import com.android.apkzlib.zip.utils.RandomAccessFileUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
@@ -40,12 +39,14 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
+import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -810,7 +811,7 @@ public class ZFile implements Closeable {
/**
* Updates the file writing new entries and removing deleted entries. This will force
* reopening the file as read/write if the file wasn't open in read/write mode.
- *
+ *
* @throws IOException failed to update the file; this exception may have been thrown by
* the compressor but only reported here
*/
@@ -2068,6 +2069,22 @@ public class ZFile implements Closeable {
}
/**
+ * Returns the current size (in bytes) of the underlying file.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public long directSize() throws IOException {
+ /*
+ * Only force a reopen if the file is closed.
+ */
+ if (mRaf == null) {
+ reopenRw();
+ assert mRaf != null;
+ }
+ return mRaf.length();
+ }
+
+ /**
* Directly reads data from the zip file. Invoking this method may force the zip to be reopened
* in read/write mode.
*
@@ -2081,17 +2098,30 @@ public class ZFile implements Closeable {
*/
public int directRead(long offset, @Nonnull byte[] data, int start, int count)
throws IOException {
- Preconditions.checkArgument(offset >= 0, "offset < 0");
Preconditions.checkArgument(start >= 0, "start >= 0");
Preconditions.checkArgument(count >= 0, "count >= 0");
+ Preconditions.checkArgument(start <= data.length, "start > data.length");
+ Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
+ return directRead(offset, ByteBuffer.wrap(data, start, count));
+ }
- if (data.length == 0) {
+ /**
+ * Directly reads data from the zip file. Invoking this method may force the zip to be reopened
+ * in read/write mode.
+ *
+ * @param offset the offset from which data should be read
+ * @param dest the output buffer to fill with data from the {@code offset}.
+ * @return how many bytes of data have been written or {@code -1} if there are no more bytes
+ * to be read
+ * @throws IOException failed to write the data
+ */
+ public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
+ Preconditions.checkArgument(offset >= 0, "offset < 0");
+
+ if (!dest.hasRemaining()) {
return 0;
}
- Preconditions.checkArgument(start <= data.length, "start > data.length");
- Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
-
/*
* Only force a reopen if the file is closed.
*/
@@ -2101,7 +2131,7 @@ public class ZFile implements Closeable {
}
mRaf.seek(offset);
- return mRaf.read(data, start, count);
+ return mRaf.getChannel().read(dest);
}
/**
@@ -2116,7 +2146,7 @@ public class ZFile implements Closeable {
}
/**
- * Reads exactly @code data.length} bytes of data, failing if it was not possible to read all
+ * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all
* the requested data.
*
* @param offset the offset at which to start reading
@@ -2124,11 +2154,42 @@ public class ZFile implements Closeable {
* @throws IOException failed to read some data or there is not enough data to read
*/
public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException {
+ directFullyRead(offset, ByteBuffer.wrap(data));
+ }
+
+ /**
+ * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read
+ * all the requested data.
+ *
+ * @param offset the offset at which to start reading
+ * @param dest the output buffer to fill with data
+ * @throws IOException failed to read some data or there is not enough data to read
+ */
+ public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
Preconditions.checkArgument(offset >= 0, "offset < 0");
- Preconditions.checkNotNull(mRaf, "File is closed");
- mRaf.seek(offset);
- RandomAccessFileUtils.fullyRead(mRaf, data);
+ if (!dest.hasRemaining()) {
+ return;
+ }
+
+ /*
+ * Only force a reopen if the file is closed.
+ */
+ if (mRaf == null) {
+ reopenRw();
+ assert mRaf != null;
+ }
+
+ FileChannel fileChannel = mRaf.getChannel();
+ while (dest.hasRemaining()) {
+ fileChannel.position(offset);
+ int chunkSize = fileChannel.read(dest);
+ if (chunkSize == -1) {
+ throw new EOFException(
+ "Failed to read " + dest.remaining() + " more bytes: premature EOF");
+ }
+ offset += chunkSize;
+ }
}
/**