diff options
author | Alex Klyubin <klyubin@google.com> | 2018-01-25 23:00:30 +0000 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2018-01-25 23:00:30 +0000 |
commit | f0708f1f3a0bd235c092755db1454a259529e727 (patch) | |
tree | 57f0f46a6ee4bc526c2cd1bf77f3eeeaa5441d3c | |
parent | cfc0b319a58800a0211f306da1d90ff901fe331d (diff) | |
parent | 4b9d059cfed959f3578c34d8810b88ee393309ea (diff) | |
download | apkzlib-f0708f1f3a0bd235c092755db1454a259529e727.tar.gz |
Use apksig's ApkSignerEngine for signing APKs am: a96ce3418b
am: 4b9d059cfe
Change-Id: Ib33785c2cc328c23a14cfb6ebcaeaf15bac1f2dd
21 files changed, 674 insertions, 1990 deletions
@@ -16,6 +16,7 @@ java_library( ]), visibility = ["//visibility:public"], deps = [ + "//tools/base/build-system:tools.apksig", "//tools/base/third_party:com.google.code.findbugs_jsr305", "//tools/base/third_party:com.google.guava_guava", "//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on", diff --git a/apkzlib.iml b/apkzlib.iml index dce8c3c..70ffee4 100644 --- a/apkzlib.iml +++ b/apkzlib.iml @@ -15,5 +15,6 @@ <orderEntry type="library" scope="TEST" name="mockito" level="project" /> <orderEntry type="library" name="bouncy-castle" level="project" /> <orderEntry type="module" module-name="testutils" /> + <orderEntry type="module" module-name="apksig" /> </component> </module> diff --git a/build.gradle b/build.gradle index aa89c1c..32ff351 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ dependencies { compile 'com.google.guava:guava:18.0' compile 'org.bouncycastle:bcpkix-jdk15on:1.48' compile 'org.bouncycastle:bcprov-jdk15on:1.48' + compile project(':apksig') testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-all:1.9.5' 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 & No FullApkSignExtension ⇒ Aligned, unsigned apk.</li> - <li>Signature Extension & No FullApkSignExtension ⇒ Aligned, v1 only signed apk.</li> - <li>Signature Extension & FullApkSignExtension ⇒ Aligned, v1 & v2 signed apk.</li> - <li>No Signature Extension & 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; + } } /** diff --git a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java index d41978a..f72f63c 100644 --- a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java +++ b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java @@ -36,7 +36,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; /** - * Tests that verify {@link FullApkSignExtension}. + * Tests that verify APK Signature Scheme v2 signing using {@link SigningExtension}. */ public class FullApkSignTest { @@ -63,9 +63,8 @@ public class FullApkSignTest { * Generate a signed zip. */ ZFile zf = new ZFile(out, options); - FullApkSignExtension signExtension = - new FullApkSignExtension(zf, 13, signData.v2, signData.v1); - signExtension.register(); + new SigningExtension(13, signData.v2, signData.v1, false, true) + .register(zf); String f1Name = "abc"; byte[] f1Data = new byte[] { 1, 1, 1, 1 }; zf.add(f1Name, new ByteArrayInputStream(f1Data)); diff --git a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java index 191bf53..ea48cfa 100644 --- a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java +++ b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java @@ -55,9 +55,7 @@ public class JarSigningTest { ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18(); - SignatureExtension signatureExtension = - new SignatureExtension(manifestExtension, 12, p.v2, p.v1, null); - signatureExtension.register(); + new SigningExtension(12, p.v2, p.v1, true, false).register(zf); } try (ZFile verifyZFile = new ZFile(zipFile)) { @@ -85,7 +83,7 @@ public class JarSigningTest { try (ZFile zf2 = new ZFile(zipFile)) { ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas"); me.register(zf2); - new SignatureExtension(me, 10, p.v2, p.v1, null).register(); + new SigningExtension(10, p.v2, p.v1, true, false).register(zf2); } try (ZFile zf3 = new ZFile(zipFile)) { @@ -121,7 +119,7 @@ public class JarSigningTest { Attributes signAttrs = signature.getAttributes("directory/file"); assertNotNull(signAttrs); assertEquals(1, signAttrs.size()); - assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", signAttrs.getValue("SHA1-Digest")); + assertEquals("LGSOwy4uGcUWoc+ZhS8ukzmf0fY=", signAttrs.getValue("SHA1-Digest")); StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA"); assertNotNull(rsaEntry); @@ -141,7 +139,7 @@ public class JarSigningTest { try (ZFile zf2 = new ZFile(zipFile)) { ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas"); me.register(zf2); - new SignatureExtension(me, 21, p.v2, p.v1, null).register(); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf2); } try (ZFile zf3 = new ZFile(zipFile)) { @@ -178,7 +176,7 @@ public class JarSigningTest { Attributes signAttrs = signature.getAttributes("directory/file"); assertNotNull(signAttrs); assertEquals(1, signAttrs.size()); - assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=", + assertEquals("dBnaLpqNjmUnLlZF4tNqOcDWL8wy8Tsw1ZYFqTZhjIs=", signAttrs.getValue("SHA-256-Digest")); StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC"); @@ -196,9 +194,7 @@ public class JarSigningTest { ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18(); - FullApkSignExtension signatureExtension = - new FullApkSignExtension(zf, 12, p.v2, p.v1); - signatureExtension.register(); + new SigningExtension(12, p.v2, p.v1, false, true).register(zf); } try (ZFile verifyZFile = new ZFile(zipFile)) { @@ -227,7 +223,7 @@ public class JarSigningTest { zf1.add(file1Name, new ByteArrayInputStream(file1Contents)); ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy); me.register(zf1); - new SignatureExtension(me, 21, p.v2, p.v1, null).register(); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf1); zf1.update(); @@ -279,7 +275,7 @@ public class JarSigningTest { try (ZFile zf2 = new ZFile(zipFile)) { ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy); me.register(zf2); - new SignatureExtension(me, 21, p.v2, p.v1, null).register(); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf2); zf2.add(file1Name, new ByteArrayInputStream(file1Contents)); @@ -312,7 +308,7 @@ public class JarSigningTest { try (ZFile zf = new ZFile(zipFile)) { ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); me.register(zf); - new SignatureExtension(me, 21, p.v2, p.v1, null).register(); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf); zf.add(fileName, new ByteArrayInputStream(fileContents)); } @@ -327,7 +323,7 @@ public class JarSigningTest { try (ZFile zf = new ZFile(zipFile)) { ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); me.register(zf); - new SignatureExtension(me, 21, p.v2, p.v1, null).register(); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf); } /* @@ -364,7 +360,7 @@ public class JarSigningTest { try (ZFile zf = new ZFile(zipFile)) { ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android"); me.register(zf); - new SignatureExtension(me, 21, p.v2, p.v1, null).register(); + new SigningExtension(21, p.v2, p.v1, true, false).register(zf); } /* |