Merge "Fix build system documentation example"
diff --git a/core/clang/arm.mk b/core/clang/arm.mk
index 4053bb2..a5472f4 100644
--- a/core/clang/arm.mk
+++ b/core/clang/arm.mk
@@ -4,12 +4,6 @@
CLANG_CONFIG_arm_EXTRA_CFLAGS :=
-ifneq (,$(filter krait,$(TARGET_$(combo_2nd_arch_prefix)CPU_VARIANT)))
- # Android's clang support's krait as a CPU whereas GCC doesn't. Specify
- # -mcpu here rather than the more normal core/combo/arch/arm/armv7-a-neon.mk.
- CLANG_CONFIG_arm_EXTRA_CFLAGS += -mcpu=krait -mfpu=neon-vfpv4
-endif
-
CLANG_CONFIG_arm_EXTRA_CPPFLAGS :=
CLANG_CONFIG_arm_EXTRA_LDFLAGS :=
@@ -31,6 +25,15 @@
-fno-tree-copy-prop \
-fno-tree-loop-optimize
+ifneq (,$(filter krait,$(TARGET_$(combo_2nd_arch_prefix)CPU_VARIANT)))
+ # Android's clang support's krait as a CPU whereas GCC doesn't. Specify
+ # -mcpu here rather than the more normal core/combo/arch/arm/armv7-a-neon.mk.
+ CLANG_CONFIG_arm_EXTRA_CFLAGS += -mcpu=krait -mfpu=neon-vfpv4
+
+ # This isn't really unknown, but allows us to only set -mcpu=krait
+ CLANG_CONFIG_arm_UNKNOWN_CFLAGS += -mcpu=cortex-a15
+endif
+
define subst-clang-incompatible-arm-flags
$(subst -march=armv5te,-march=armv5t,\
$(subst -march=armv5e,-march=armv5,\
diff --git a/core/clang/mips.mk b/core/clang/mips.mk
index 4a8f812..aeb2f6a 100644
--- a/core/clang/mips.mk
+++ b/core/clang/mips.mk
@@ -15,11 +15,6 @@
-mno-synci \
-mno-fused-madd
-# Temporary workaround for Mips clang++ problem, creates
-# relocated ptrs in read-only pic .gcc_exception_table;
-# permanent fix pending at http://reviews.llvm.org/D9669
-CLANG_CONFIG_mips_UNKNOWN_CFLAGS += -Wl,--warn-shared-textrel
-
# We don't have any mips flags to substitute yet.
define subst-clang-incompatible-mips-flags
$(1)
diff --git a/core/clang/mips64.mk b/core/clang/mips64.mk
index 1b72e05..20e87bd 100644
--- a/core/clang/mips64.mk
+++ b/core/clang/mips64.mk
@@ -15,11 +15,6 @@
-mno-synci \
-mno-fused-madd
-# Temporary workaround for Mips clang++ problem creating
-# relocated ptrs in read-only pic .gcc_exception_table;
-# permanent fix pending at http://reviews.llvm.org/D9669
-CLANG_CONFIG_mips64_UNKNOWN_CFLAGS += -Wl,--warn-shared-textrel
-
# We don't have any mips64 flags to substitute yet.
define subst-clang-incompatible-mips64-flags
$(1)
diff --git a/core/combo/TARGET_linux-arm.mk b/core/combo/TARGET_linux-arm.mk
index d89a2cf..9be6c73 100644
--- a/core/combo/TARGET_linux-arm.mk
+++ b/core/combo/TARGET_linux-arm.mk
@@ -119,16 +119,6 @@
-fno-strict-volatile-bitfields
endif
-# This is to avoid the dreaded warning compiler message:
-# note: the mangling of 'va_list' has changed in GCC 4.4
-#
-# The fact that the mangling changed does not affect the NDK ABI
-# very fortunately (since none of the exposed APIs used va_list
-# in their exported C++ functions). Also, GCC 4.5 has already
-# removed the warning from the compiler.
-#
-$(combo_2nd_arch_prefix)TARGET_GLOBAL_CFLAGS += -Wno-psabi
-
$(combo_2nd_arch_prefix)TARGET_GLOBAL_LDFLAGS += \
-Wl,-z,noexecstack \
-Wl,-z,relro \
diff --git a/core/combo/TARGET_linux-arm64.mk b/core/combo/TARGET_linux-arm64.mk
index 9ff4981..61028c4 100644
--- a/core/combo/TARGET_linux-arm64.mk
+++ b/core/combo/TARGET_linux-arm64.mk
@@ -95,16 +95,6 @@
TARGET_GLOBAL_CFLAGS += -fno-strict-volatile-bitfields
-# This is to avoid the dreaded warning compiler message:
-# note: the mangling of 'va_list' has changed in GCC 4.4
-#
-# The fact that the mangling changed does not affect the NDK ABI
-# very fortunately (since none of the exposed APIs used va_list
-# in their exported C++ functions). Also, GCC 4.5 has already
-# removed the warning from the compiler.
-#
-TARGET_GLOBAL_CFLAGS += -Wno-psabi
-
TARGET_GLOBAL_LDFLAGS += \
-Wl,-z,noexecstack \
-Wl,-z,relro \
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
new file mode 100644
index 0000000..cb0f84a
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.apk.v2;
+
+/**
+ * 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 mJcaMessageDigestAlgorithm;
+ private final int mChunkDigestOutputSizeBytes;
+
+ private ContentDigestAlgorithm(
+ String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+ mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
+ mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
+ }
+
+ /**
+ * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
+ * chunks by this content digest algorithm.
+ */
+ String getJcaMessageDigestAlgorithm() {
+ return mJcaMessageDigestAlgorithm;
+ }
+
+ /**
+ * 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/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
new file mode 100644
index 0000000..182b4ed
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apksigner.core.internal.apk.v2;
+
+import com.android.apksigner.core.util.DataSink;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+/**
+ * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
+ * {@code MessageDigest} instance receives the same data.
+ */
+class MessageDigestSink implements DataSink {
+
+ private final MessageDigest[] mMessageDigests;
+
+ MessageDigestSink(MessageDigest[] digests) {
+ mMessageDigests = digests;
+ }
+
+ @Override
+ public void consume(byte[] buf, int offset, int length) {
+ for (MessageDigest md : mMessageDigests) {
+ md.update(buf, offset, length);
+ }
+ }
+
+ @Override
+ public void consume(ByteBuffer buf) {
+ int originalPosition = buf.position();
+ for (MessageDigest md : mMessageDigests) {
+ // Reset the position back to the original because the previous iteration's
+ // MessageDigest.update set the buffer's position to the buffer's limit.
+ buf.position(originalPosition);
+ md.update(buf);
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
new file mode 100644
index 0000000..3c7b5f0
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.apk.v2;
+
+import com.android.apksigner.core.internal.util.Pair;
+
+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,
+ "RSA",
+ Pair.of("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,
+ "RSA",
+ Pair.of(
+ "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,
+ "RSA",
+ Pair.of("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,
+ "RSA",
+ Pair.of("SHA512withRSA", null)),
+
+ /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ ECDSA_WITH_SHA256(
+ 0x0201,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "EC",
+ Pair.of("SHA256withECDSA", null)),
+
+ /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+ ECDSA_WITH_SHA512(
+ 0x0202,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "EC",
+ Pair.of("SHA512withECDSA", null)),
+
+ /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ DSA_WITH_SHA256(
+ 0x0301,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDSA", null));
+
+ private final int mId;
+ private final String mJcaKeyAlgorithm;
+ private final ContentDigestAlgorithm mContentDigestAlgorithm;
+ private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
+
+ private SignatureAlgorithm(int id,
+ ContentDigestAlgorithm contentDigestAlgorithm,
+ String jcaKeyAlgorithm,
+ Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
+ mId = id;
+ mContentDigestAlgorithm = contentDigestAlgorithm;
+ mJcaKeyAlgorithm = jcaKeyAlgorithm;
+ 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 JCA {@link java.security.Key} algorithm used by this signature scheme.
+ */
+ String getJcaKeyAlgorithm() {
+ return mJcaKeyAlgorithm;
+ }
+
+ /**
+ * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
+ * (or null if not needed) to parameterize the {@code Signature}.
+ */
+ Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
+ return mJcaSignatureAlgAndParams;
+ }
+
+ static SignatureAlgorithm findById(int id) {
+ for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+ if (alg.getId() == id) {
+ return alg;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
new file mode 100644
index 0000000..e185346
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.apk.v2;
+
+import com.android.apksigner.core.internal.util.ByteBufferSink;
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.internal.zip.ZipUtils;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.util.DataSources;
+
+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.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme v2 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 V2SchemeSigner {
+ /*
+ * 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;
+
+ /**
+ * Signer configuration.
+ */
+ public static class SignerConfig {
+ /** Private key. */
+ public PrivateKey privateKey;
+
+ /**
+ * Certificates, with the first certificate containing the public key corresponding to
+ * {@link #privateKey}.
+ */
+ public List<X509Certificate> certificates;
+
+ /**
+ * List of signature algorithms with which to sign.
+ */
+ public List<SignatureAlgorithm> signatureAlgorithms;
+ }
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SchemeSigner() {}
+
+ /**
+ * 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(
+ 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 Collections.singletonList(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 Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
+ }
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ // DSA is supported only with SHA-256.
+ return Collections.singletonList(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 Collections.singletonList(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 Collections.singletonList(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.
+ *
+ * @param signerConfigs signer configurations, one for each signer At least one signer config
+ * must be provided.
+ *
+ * @throws IOException if an I/O error occurs
+ * @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
+ */
+ public static byte[] generateApkSigningBlock(
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs)
+ throws IOException, 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 = new HashSet<>(1);
+ for (SignerConfig signerConfig : signerConfigs) {
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
+ }
+ }
+
+ // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
+ // offset field is treated as pointing to the offset at which the APK Signing Block will
+ // start.
+ long centralDirOffsetForDigesting = beforeCentralDir.size();
+ ByteBuffer eocdBuf = copyToByteBuffer(eocd);
+ eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
+
+ // Compute digests of APK contents.
+ Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
+ try {
+ contentDigests =
+ computeContentDigests(
+ contentDigestAlgorithms,
+ new DataSource[] {
+ beforeCentralDir,
+ centralDir,
+ DataSources.asDataSource(eocdBuf)});
+ } catch (IOException e) {
+ throw new IOException("Failed to read APK being signed", e);
+ } 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);
+ }
+
+ private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+ Set<ContentDigestAlgorithm> digestAlgorithms,
+ DataSource[] contents) throws IOException, 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 (DataSource 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 jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+ try {
+ mds[i] = MessageDigest.getInstance(jcaAlgorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(jcaAlgorithm + " MessageDigest not supported", e);
+ }
+ }
+
+ MessageDigestSink mdSink = new MessageDigestSink(mds);
+ 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 (DataSource 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.feed(inputOffset, chunkSize, mdSink);
+ } catch (IOException e) {
+ throw new IOException("Failed to read 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 = new HashMap<>(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 = new ArrayList<>(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<Pair<Integer, byte[]>> digests =
+ new ArrayList<>(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(Pair.of(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 = new ArrayList<>(signerConfig.signatureAlgorithms.size());
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
+ String jcaSignatureAlgorithm = sigAlgAndParams.getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond();
+ 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(Pair.of(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<Pair<Integer, byte[]>> signatures;
+ public byte[] publicKey;
+ }
+
+ private static final class SignedData {
+ public List<Pair<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 = new ArrayList<>(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<Pair<Integer, byte[]>> sequence) {
+ int resultSize = 0;
+ for (Pair<Integer, byte[]> element : sequence) {
+ resultSize += 12 + element.getSecond().length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (Pair<Integer, byte[]> element : sequence) {
+ byte[] second = element.getSecond();
+ result.putInt(8 + second.length);
+ result.putInt(element.getFirst());
+ result.putInt(second.length);
+ result.put(second);
+ }
+ return result.array();
+ }
+
+ private static ByteBuffer copyToByteBuffer(DataSource dataSource) throws IOException {
+ long dataSourceSize = dataSource.size();
+ if (dataSourceSize > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("Data source too large: " + dataSourceSize);
+ }
+ ByteBuffer result = ByteBuffer.allocate((int) dataSourceSize);
+ dataSource.feed(0, result.remaining(), new ByteBufferSink(result));
+ result.position(0);
+ return result;
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
new file mode 100644
index 0000000..76f4fda
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferDataSource.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.util;
+
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * {@link DataSource} backed by a {@link ByteBuffer}.
+ */
+public class ByteBufferDataSource implements DataSource {
+
+ private final ByteBuffer mBuffer;
+ private final long mSize;
+
+ /**
+ * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
+ * buffer between the buffer's position and limit.
+ */
+ public ByteBufferDataSource(ByteBuffer buffer) {
+ mBuffer = buffer.slice();
+ mSize = buffer.remaining();
+ }
+
+ @Override
+ public long size() {
+ return mSize;
+ }
+
+ @Override
+ public void feed(long offset, int size, DataSink sink) throws IOException {
+ if (offset < 0) {
+ throw new IllegalArgumentException("offset: " + offset);
+ }
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ if (offset > mSize) {
+ throw new IllegalArgumentException(
+ "offset (" + offset + ") > source size (" + mSize + ")");
+ }
+ long endOffset = offset + size;
+ if (endOffset < offset) {
+ throw new IllegalArgumentException(
+ "offset (" + offset + ") + size (" + size + ") overflow");
+ }
+ if (endOffset > mSize) {
+ throw new IllegalArgumentException(
+ "offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
+ }
+
+ int chunkPosition = (int) offset; // safe to downcast because mSize <= Integer.MAX_VALUE
+ int chunkLimit = (int) endOffset; // safe to downcast because mSize <= Integer.MAX_VALUE
+ ByteBuffer chunk;
+ // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
+ // and limit fields, to be more specific). We thus use synchronization around these
+ // state-changing operations to make instances of this class thread-safe.
+ synchronized (mBuffer) {
+ // ByteBuffer.limit(int) and .position(int) check that that the position >= limit
+ // invariant is not broken. Thus, the only way to safely change position and limit
+ // without caring about their current values is to first set position to 0 or set the
+ // limit to capacity.
+ mBuffer.position(0);
+
+ mBuffer.limit(chunkLimit);
+ mBuffer.position(chunkPosition);
+ chunk = mBuffer.slice();
+ }
+
+ sink.consume(chunk);
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java
new file mode 100644
index 0000000..8c57905
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/ByteBufferSink.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.util;
+
+import com.android.apksigner.core.util.DataSink;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * Data sink which stores all received data into the associated {@link ByteBuffer}.
+ */
+public class ByteBufferSink implements DataSink {
+
+ private final ByteBuffer mBuffer;
+
+ public ByteBufferSink(ByteBuffer buffer) {
+ mBuffer = buffer;
+ }
+
+ @Override
+ public void consume(byte[] buf, int offset, int length) throws IOException {
+ try {
+ mBuffer.put(buf, offset, length);
+ } catch (BufferOverflowException e) {
+ throw new IOException(
+ "Insufficient space in output buffer for " + length + " bytes", e);
+ }
+ }
+
+ @Override
+ public void consume(ByteBuffer buf) throws IOException {
+ int length = buf.remaining();
+ try {
+ mBuffer.put(buf);
+ } catch (BufferOverflowException e) {
+ throw new IOException(
+ "Insufficient space in output buffer for " + length + " bytes", e);
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
new file mode 100644
index 0000000..7b47e50
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.zip;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Assorted ZIP format helpers.
+ *
+ * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
+ * order of these buffers is little-endian.
+ */
+public abstract class ZipUtils {
+ private ZipUtils() {}
+
+ private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
+
+ /**
+ * Sets the offset of the start of the ZIP Central Directory in the archive.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static void setZipEocdCentralDirectoryOffset(
+ ByteBuffer zipEndOfCentralDirectory, long offset) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ setUnsignedInt32(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
+ offset);
+ }
+
+ private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
+ if ((value < 0) || (value > 0xffffffffL)) {
+ throw new IllegalArgumentException("uint32 value of out range: " + value);
+ }
+ buffer.putInt(buffer.position() + offset, (int) value);
+ }
+}
\ No newline at end of file
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java
new file mode 100644
index 0000000..35a61fc
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSink.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Consumer of input data which may be provided in one go or in chunks.
+ */
+public interface DataSink {
+
+ /**
+ * Consumes the provided chunk of data.
+ *
+ * <p>This data sink guarantees to not hold references to the provided buffer after this method
+ * terminates.
+ */
+ void consume(byte[] buf, int offset, int length) throws IOException;
+
+ /**
+ * Consumes all remaining data in the provided buffer and advances the buffer's position
+ * to the buffer's limit.
+ *
+ * <p>This data sink guarantees to not hold references to the provided buffer after this method
+ * terminates.
+ */
+ void consume(ByteBuffer buf) throws IOException;
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
new file mode 100644
index 0000000..04560cb
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSource.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.util;
+
+import java.io.IOException;
+
+/**
+ * Abstract representation of a source of data.
+ *
+ * <p>This abstraction serves three purposes:
+ * <ul>
+ * <li>Transparent handling of different types of sources, such as {@code byte[]},
+ * {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.</li>
+ * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
+ * may have worked as the unifying abstraction.</li>
+ * <li>Support sources which do not fit into logical memory as a contiguous region.</li>
+ * </ul>
+ */
+public interface DataSource {
+
+ /**
+ * Returns the amount of data (in bytes) contained in this data source.
+ */
+ long size();
+
+ /**
+ * Feeds the specified chunk from this data source into the provided sink.
+ *
+ * @param offset index (in bytes) at which the chunk starts inside data source
+ * @param size size (in bytes) of the chunk
+ */
+ void feed(long offset, int size, DataSink sink) throws IOException;
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
new file mode 100644
index 0000000..978afae
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSources.java
@@ -0,0 +1,23 @@
+package com.android.apksigner.core.util;
+
+import com.android.apksigner.core.internal.util.ByteBufferDataSource;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for working with {@link DataSource} abstraction.
+ */
+public abstract class DataSources {
+ private DataSources() {}
+
+ /**
+ * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source
+ * represents the data contained between the position and limit of the buffer.
+ */
+ public static DataSource asDataSource(ByteBuffer buffer) {
+ if (buffer == null) {
+ throw new NullPointerException();
+ }
+ return new ByteBufferDataSource(buffer);
+ }
+}