Merge "APK Signature Scheme v2 signing logic for apksigner-core."
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user