/*
 * Copyright (C) 2018 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.apksig.internal.apk;

import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256;
import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512;
import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;

import com.android.apksig.ApkVerifier;
import com.android.apksig.KeyConfig;
import com.android.apksig.SignerEngineFactory;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.asn1.Asn1BerParser;
import com.android.apksig.internal.asn1.Asn1DecodingException;
import com.android.apksig.internal.asn1.Asn1DerEncoder;
import com.android.apksig.internal.asn1.Asn1EncodingException;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
import com.android.apksig.internal.pkcs7.ContentInfo;
import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
import com.android.apksig.internal.pkcs7.Pkcs7Constants;
import com.android.apksig.internal.pkcs7.SignedData;
import com.android.apksig.internal.pkcs7.SignerIdentifier;
import com.android.apksig.internal.pkcs7.SignerInfo;
import com.android.apksig.internal.util.ByteBufferDataSource;
import com.android.apksig.internal.util.ChainedDataSource;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.util.VerityTreeBuilder;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.x509.RSAPublicKey;
import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.util.RunnablesExecutor;

import java.io.IOException;
import java.math.BigInteger;
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.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import javax.security.auth.x500.X500Principal;

public class ApkSigningBlockUtils {

    private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
    public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
    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,
          };
    public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;

    private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
            {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};

    public static final int VERSION_SOURCE_STAMP = 0;
    public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
    public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
    public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
    public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
    public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;

    /**
     * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
     * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
     */
    public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
        return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2);
    }

    /**
     * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
     * APK and comparing them against the digests listed in APK Signing Block. The expected digests
     * are taken from {@code SignerInfos} of the provided {@code result}.
     *
     * <p>This method adds one or more errors to the {@code result} if a verification error is
     * expected to be encountered on Android. No errors are added to the {@code result} if the APK's
     * integrity is expected to verify on Android for each algorithm in
     * {@code contentDigestAlgorithms}.
     *
     * <p>The reason this method is currently not parameterized by a
     * {@code [minSdkVersion, maxSdkVersion]} range is that up until now content digest algorithms
     * exhibit the same behavior on all Android platform versions.
     */
    public static void verifyIntegrity(
            RunnablesExecutor executor,
            DataSource beforeApkSigningBlock,
            DataSource centralDir,
            ByteBuffer eocd,
            Set<ContentDigestAlgorithm> contentDigestAlgorithms,
            Result result) throws IOException, NoSuchAlgorithmException {
        if (contentDigestAlgorithms.isEmpty()) {
            // This should never occur because this method is invoked once at least one signature
            // is verified, meaning at least one content digest is known.
            throw new RuntimeException("No content digests found");
        }

        // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
        // treated as though its Central Directory offset points to the start of APK Signing Block.
        // We thus modify the EoCD accordingly.
        ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
        int eocdSavedPos = eocd.position();
        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
        modifiedEocd.put(eocd);
        modifiedEocd.flip();

        // restore eocd to position prior to modification in case it is to be used elsewhere
        eocd.position(eocdSavedPos);
        ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
        Map<ContentDigestAlgorithm, byte[]> actualContentDigests;
        try {
            actualContentDigests =
                    computeContentDigests(
                            executor,
                            contentDigestAlgorithms,
                            beforeApkSigningBlock,
                            centralDir,
                            new ByteBufferDataSource(modifiedEocd));
            // Special checks for the verity algorithm requirements.
            if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) {
                if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
                    throw new RuntimeException(
                            "APK Signing Block is not aligned on 4k boundary: " +
                            beforeApkSigningBlock.size());
                }

                long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
                long signingBlockSize = centralDirOffset - beforeApkSigningBlock.size();
                if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
                    throw new RuntimeException(
                            "APK Signing Block size is not multiple of page size: " +
                            signingBlockSize);
                }
            }
        } catch (DigestException e) {
            throw new RuntimeException("Failed to compute content digests", e);
        }
        if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
            throw new RuntimeException(
                    "Mismatch between sets of requested and computed content digests"
                            + " . Requested: " + contentDigestAlgorithms
                            + ", computed: " + actualContentDigests.keySet());
        }

        // Compare digests computed over the rest of APK against the corresponding expected digests
        // in signer blocks.
        for (Result.SignerInfo signerInfo : result.signers) {
            for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
                SignatureAlgorithm signatureAlgorithm =
                        SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
                if (signatureAlgorithm == null) {
                    continue;
                }
                ContentDigestAlgorithm contentDigestAlgorithm =
                        signatureAlgorithm.getContentDigestAlgorithm();
                // if the current digest algorithm is not in the list provided by the caller then
                // ignore it; the signer may contain digests not recognized by the specified SDK
                // range.
                if (!contentDigestAlgorithms.contains(contentDigestAlgorithm)) {
                    continue;
                }
                byte[] expectedDigest = expected.getValue();
                byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
                if (!Arrays.equals(expectedDigest, actualDigest)) {
                    if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
                        signerInfo.addError(
                                ApkVerifier.Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
                                contentDigestAlgorithm,
                                toHex(expectedDigest),
                                toHex(actualDigest));
                    } else if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3) {
                        signerInfo.addError(
                                ApkVerifier.Issue.V3_SIG_APK_DIGEST_DID_NOT_VERIFY,
                                contentDigestAlgorithm,
                                toHex(expectedDigest),
                                toHex(actualDigest));
                    }
                    continue;
                }
                signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
            }
        }
    }

    public static ByteBuffer findApkSignatureSchemeBlock(
            ByteBuffer apkSigningBlock,
            int blockId,
            Result result) throws SignatureNotFoundException {
        try {
            return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId);
        } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
            throw new SignatureNotFoundException(e.getMessage());
        }
    }

    public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
        ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer);
    }

    public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
        return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source);
    }

    public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
        return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf);
    }

    public static String toHex(byte[] value) {
        return ApkSigningBlockUtilsLite.toHex(value);
    }

    public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
            RunnablesExecutor executor,
            Set<ContentDigestAlgorithm> digestAlgorithms,
            DataSource beforeCentralDir,
            DataSource centralDir,
            DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException {
        Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>();
        Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>();
        for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) {
            if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
                    || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
                oneMbChunkBasedAlgorithm.add(digestAlgorithm);
            }
        }
        computeOneMbChunkContentDigests(
                executor,
                oneMbChunkBasedAlgorithm,
                new DataSource[] { beforeCentralDir, centralDir, eocd },
                contentDigests);

        if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
            computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
        }
        return contentDigests;
    }

    static void computeOneMbChunkContentDigests(
            Set<ContentDigestAlgorithm> digestAlgorithms,
            DataSource[] contents,
            Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
            throws IOException, NoSuchAlgorithmException, 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();
            mds[i] = MessageDigest.getInstance(jcaAlgorithm);
        }

        DataSink mdSink = DataSinks.asDataSink(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++;
            }
        }

        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);
            outputContentDigests.put(digestAlgorithm, digest);
        }
    }

    static void computeOneMbChunkContentDigests(
            RunnablesExecutor executor,
            Set<ContentDigestAlgorithm> digestAlgorithms,
            DataSource[] contents,
            Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
            throws NoSuchAlgorithmException, DigestException {
        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;

        List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
        for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
            chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
        }

        ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
        executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList));

        // Compute and write out final digest for each algorithm.
        for (ChunkDigests chunkDigests : chunkDigestsList) {
            MessageDigest messageDigest = chunkDigests.createMessageDigest();
            outputContentDigests.put(
                    chunkDigests.algorithm,
                    messageDigest.digest(chunkDigests.concatOfDigestsOfChunks));
        }
    }

    private static class ChunkDigests {
        private final ContentDigestAlgorithm algorithm;
        private final int digestOutputSize;
        private final byte[] concatOfDigestsOfChunks;

        private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) {
            this.algorithm = algorithm;
            digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes();
            concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize];

            // Fill the initial values of the concatenated digests of chunks, which is
            // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
            concatOfDigestsOfChunks[0] = 0x5a;
            setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1);
        }

        private MessageDigest createMessageDigest() throws NoSuchAlgorithmException {
            return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm());
        }

        private int getOffset(int chunkIndex) {
            return 1 + 4 + chunkIndex * digestOutputSize;
        }
    }

    /**
     * A per-thread digest worker.
     */
    private static class ChunkDigester implements Runnable {
        private final ChunkSupplier dataSupplier;
        private final List<ChunkDigests> chunkDigests;
        private final List<MessageDigest> messageDigests;
        private final DataSink mdSink;

        private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests) {
            this.dataSupplier = dataSupplier;
            this.chunkDigests = chunkDigests;
            messageDigests = new ArrayList<>(chunkDigests.size());
            for (ChunkDigests chunkDigest : chunkDigests) {
                try {
                    messageDigests.add(chunkDigest.createMessageDigest());
                } catch (NoSuchAlgorithmException ex) {
                    throw new RuntimeException(ex);
                }
            }
            mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0]));
        }

        @Override
        public void run() {
            byte[] chunkContentPrefix = new byte[5];
            chunkContentPrefix[0] = (byte) 0xa5;

            try {
                for (ChunkSupplier.Chunk chunk = dataSupplier.get();
                     chunk != null;
                     chunk = dataSupplier.get()) {
                    int size = chunk.size;
                    if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
                        throw new RuntimeException("Chunk size greater than expected: " + size);
                    }

                    // First update with the chunk prefix.
                    setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1);
                    mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);

                    // Then update with the chunk data.
                    mdSink.consume(chunk.data);

                    // Now finalize chunk for all algorithms.
                    for (int i = 0; i < chunkDigests.size(); i++) {
                        ChunkDigests chunkDigest = chunkDigests.get(i);
                        int actualDigestSize = messageDigests.get(i).digest(
                                chunkDigest.concatOfDigestsOfChunks,
                                chunkDigest.getOffset(chunk.chunkIndex),
                                chunkDigest.digestOutputSize);
                        if (actualDigestSize != chunkDigest.digestOutputSize) {
                            throw new RuntimeException(
                                    "Unexpected output size of " + chunkDigest.algorithm
                                            + " digest: " + actualDigestSize);
                        }
                    }
                }
            } catch (IOException | DigestException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a
     * supplied {@link DataSource}, the data from the next {@link DataSource}
     * are NOT concatenated. Only the next call to get() will fetch from the
     * next {@link DataSource} in the input {@link DataSource} array.
     */
    private static class ChunkSupplier implements Supplier<ChunkSupplier.Chunk> {
        private final DataSource[] dataSources;
        private final int[] chunkCounts;
        private final int totalChunkCount;
        private final AtomicInteger nextIndex;

        private ChunkSupplier(DataSource[] dataSources) {
            this.dataSources = dataSources;
            chunkCounts = new int[dataSources.length];
            int totalChunkCount = 0;
            for (int i = 0; i < dataSources.length; i++) {
                long chunkCount = getChunkCount(dataSources[i].size(),
                        CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
                if (chunkCount > Integer.MAX_VALUE) {
                    throw new RuntimeException(
                            String.format(
                                    "Number of chunks in dataSource[%d] is greater than max int.",
                                    i));
                }
                chunkCounts[i] = (int)chunkCount;
                totalChunkCount = (int) (totalChunkCount + chunkCount);
            }
            this.totalChunkCount = totalChunkCount;
            nextIndex = new AtomicInteger(0);
        }

        /**
         * We map an integer index to the termination-adjusted dataSources 1MB chunks.
         * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned
         * blocks in each input {@link DataSource} (unless the DataSource itself is
         * 1MB-aligned).
         */
        @Override
        public ChunkSupplier.Chunk get() {
            int index = nextIndex.getAndIncrement();
            if (index < 0 || index >= totalChunkCount) {
                return null;
            }

            int dataSourceIndex = 0;
            long dataSourceChunkOffset = index;
            for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
                if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
                    break;
                }
                dataSourceChunkOffset -= chunkCounts[dataSourceIndex];
            }

            long remainingSize = Math.min(
                    dataSources[dataSourceIndex].size() -
                            dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
                    CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);

            final int size = (int)remainingSize;
            final ByteBuffer buffer = ByteBuffer.allocate(size);
            try {
                dataSources[dataSourceIndex].copyTo(
                        dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size,
                        buffer);
            } catch (IOException e) {
                throw new IllegalStateException("Failed to read chunk", e);
            }
            buffer.rewind();

            return new Chunk(index, buffer, size);
        }

        static class Chunk {
            private final int chunkIndex;
            private final ByteBuffer data;
            private final int size;

            private Chunk(int chunkIndex, ByteBuffer data, int size) {
                this.chunkIndex = chunkIndex;
                this.data = data;
                this.size = size;
            }
        }
    }

    @SuppressWarnings("ByteBufferBackingArray")
    private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
            DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
            throws IOException, NoSuchAlgorithmException {
        ByteBuffer encoded = createVerityDigestBuffer(true);
        // Use 0s as salt for now.  This also needs to be consistent in the fsverify header for
        // kernel to use.
        try (VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8])) {
            byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir,
                    eocd);
            encoded.put(rootHash);
            encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
            outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array());
        }
    }

    private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) {
        // FORMAT:
        // OFFSET       DATA TYPE  DESCRIPTION
        // * @+0  bytes uint8[32]  Merkle tree root hash of SHA-256
        // * @+32 bytes int64      (optional) Length of source data
        int backBufferSize =
                VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
        if (includeSourceDataSize) {
            backBufferSize += Long.SIZE / Byte.SIZE;
        }
        ByteBuffer encoded = ByteBuffer.allocate(backBufferSize);
        encoded.order(ByteOrder.LITTLE_ENDIAN);
        return encoded;
    }

    public static class VerityTreeAndDigest {
        public final ContentDigestAlgorithm contentDigestAlgorithm;
        public final byte[] rootHash;
        public final byte[] tree;

        VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash,
                byte[] tree) {
            this.contentDigestAlgorithm = contentDigestAlgorithm;
            this.rootHash = rootHash;
            this.tree = tree;
        }
    }

    @SuppressWarnings("ByteBufferBackingArray")
    public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
            throws IOException, NoSuchAlgorithmException {
        ByteBuffer encoded = createVerityDigestBuffer(false);
        // Use 0s as salt for now.  This also needs to be consistent in the fsverify header for
        // kernel to use.
        try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
            ByteBuffer tree = builder.generateVerityTree(dataSource);
            byte[] rootHash = builder.getRootHashFromTree(tree);
            encoded.put(rootHash);
            return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array());
        }
    }

    private static long getChunkCount(long inputSize, long 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);
    }

    public static byte[] encodePublicKey(PublicKey publicKey)
            throws InvalidKeyException, NoSuchAlgorithmException {
        byte[] encodedPublicKey = null;
        if ("X.509".equals(publicKey.getFormat())) {
            encodedPublicKey = publicKey.getEncoded();
            // if the key is an RSA key check for a negative modulus
            String keyAlgorithm = publicKey.getAlgorithm();
            if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
                try {
                    // Parse the encoded public key into the separate elements of the
                    // SubjectPublicKeyInfo to obtain the SubjectPublicKey.
                    ByteBuffer encodedPublicKeyBuffer = ByteBuffer.wrap(encodedPublicKey);
                    SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse(
                            encodedPublicKeyBuffer, SubjectPublicKeyInfo.class);
                    // The SubjectPublicKey is encoded as a bit string within the
                    // SubjectPublicKeyInfo. The first byte of the encoding is the number of padding
                    // bits; store this and decode the rest of the bit string into the RSA modulus
                    // and exponent.
                    ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey;
                    byte padding = subjectPublicKeyBuffer.get();
                    RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer,
                            RSAPublicKey.class);
                    // if the modulus is negative then attempt to reencode it with a leading 0 sign
                    // byte.
                    if (rsaPublicKey.modulus.compareTo(BigInteger.ZERO) < 0) {
                        // A negative modulus indicates the leading bit in the integer is 1. Per
                        // ASN.1 encoding rules to encode a positive integer with the leading bit
                        // set to 1 a byte containing all zeros should precede the integer encoding.
                        byte[] encodedModulus = rsaPublicKey.modulus.toByteArray();
                        byte[] reencodedModulus = new byte[encodedModulus.length + 1];
                        reencodedModulus[0] = 0;
                        System.arraycopy(encodedModulus, 0, reencodedModulus, 1,
                                encodedModulus.length);
                        rsaPublicKey.modulus = new BigInteger(reencodedModulus);
                        // Once the modulus has been corrected reencode the RSAPublicKey, then
                        // restore the padding value in the bit string and reencode the entire
                        // SubjectPublicKeyInfo to be returned to the caller.
                        byte[] reencodedRSAPublicKey = Asn1DerEncoder.encode(rsaPublicKey);
                        byte[] reencodedSubjectPublicKey =
                                new byte[reencodedRSAPublicKey.length + 1];
                        reencodedSubjectPublicKey[0] = padding;
                        System.arraycopy(reencodedRSAPublicKey, 0, reencodedSubjectPublicKey, 1,
                                reencodedRSAPublicKey.length);
                        subjectPublicKeyInfo.subjectPublicKey = ByteBuffer.wrap(
                                reencodedSubjectPublicKey);
                        encodedPublicKey = Asn1DerEncoder.encode(subjectPublicKeyInfo);
                    }
                } catch (Asn1DecodingException | Asn1EncodingException e) {
                    System.out.println("Caught a exception encoding the public key: " + e);
                    e.printStackTrace();
                    encodedPublicKey = null;
                }
            }
        }
        if (encodedPublicKey == null) {
            try {
                encodedPublicKey =
                        KeyFactory.getInstance(publicKey.getAlgorithm())
                                .getKeySpec(publicKey, X509EncodedKeySpec.class)
                                .getEncoded();
            } 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;
    }

    public 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;
    }

    public static byte[] encodeAsLengthPrefixedElement(byte[] bytes) {
        byte[][] adapterBytes = new byte[1][];
        adapterBytes[0] = bytes;
        return encodeAsSequenceOfLengthPrefixedElements(adapterBytes);
    }

    public static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
        return encodeAsSequenceOfLengthPrefixedElements(
                sequence.toArray(new byte[sequence.size()][]));
    }

    public 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();
      }

    public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
            List<Pair<Integer, byte[]>> sequence) {
        return ApkSigningBlockUtilsLite
                .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence);
      }

    /**
     * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
     * and the additional information relevant for verifying the block against the file.
     *
     * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
     *                identifying the appropriate block to find, e.g. the APK Signature Scheme v2
     *                block ID.
     *
     * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
     * @throws IOException if an I/O error occurs while reading the APK
     */
    public static SignatureInfo findSignature(
            DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result)
                    throws IOException, SignatureNotFoundException {
        try {
            return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId);
        } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
            throw new SignatureNotFoundException(e.getMessage());
        }
    }

    /**
     * Generates a new DataSource representing the APK contents before the Central Directory with
     * padding, if padding is requested.  If the existing data entries before the Central Directory
     * are already aligned, or no padding is requested, the original DataSource is used.  This
     * padding is used to allow for verity-based APK verification.
     *
     * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of
     *         padding used.
     */
    public static Pair<DataSource, Integer> generateApkSigningBlockPadding(
            DataSource beforeCentralDir,
            boolean apkSigningBlockPaddingSupported) {

        // Ensure APK Signing Block starts from page boundary.
        int padSizeBeforeSigningBlock = 0;
        if (apkSigningBlockPaddingSupported &&
                (beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
            padSizeBeforeSigningBlock = (int) (
                    ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
                            beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
            beforeCentralDir = new ChainedDataSource(
                    beforeCentralDir,
                    DataSources.asDataSource(
                            ByteBuffer.allocate(padSizeBeforeSigningBlock)));
        }
        return Pair.of(beforeCentralDir, padSizeBeforeSigningBlock);
    }

    public static DataSource copyWithModifiedCDOffset(
            DataSource beforeCentralDir, DataSource eocd) throws IOException {

        // 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 = ByteBuffer.allocate((int) eocd.size());
        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
        eocd.copyTo(0, (int) eocd.size(), eocdBuf);
        eocdBuf.flip();
        ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
        return DataSources.asDataSource(eocdBuf);
    }

    public static byte[] generateApkSigningBlock(
            List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) {
        // FORMAT:
        // uint64:  size (excluding this field)
        // repeated ID-value pairs:
        //     uint64:           size (excluding this field)
        //     uint32:           ID
        //     (size - 4) bytes: value
        // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
        // uint64:  size (same as the one above)
        // uint128: magic

        int blocksSize = 0;
        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
            blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value
        }

        int resultSize =
                8 // size
                + blocksSize
                + 8 // size
                + 16 // magic
                ;
        ByteBuffer paddingPair = null;
        if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
            int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
                    (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
            if (padding < 12) {  // minimum size of an ID-value pair
                padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
            }
            paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
            paddingPair.putLong(padding - 8);
            paddingPair.putInt(VERITY_PADDING_BLOCK_ID);
            paddingPair.rewind();
            resultSize += padding;
        }

        ByteBuffer result = ByteBuffer.allocate(resultSize);
        result.order(ByteOrder.LITTLE_ENDIAN);
        long blockSizeFieldValue = resultSize - 8L;
        result.putLong(blockSizeFieldValue);

        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
            byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
            int apkSignatureSchemeId = schemeBlockPair.getSecond();
            long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length;
            result.putLong(pairSizeFieldValue);
            result.putInt(apkSignatureSchemeId);
            result.put(apkSignatureSchemeBlock);
        }

        if (paddingPair != null) {
            result.put(paddingPair);
        }

        result.putLong(blockSizeFieldValue);
        result.put(APK_SIGNING_BLOCK_MAGIC);

        return result.array();
    }

    /**
     * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a
     * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the
     * contents / value of the signature block and the second element is the ID of the block.
     *
     * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock}
     */
    public static List<Pair<byte[], Integer>> getApkSignatureBlocks(
            DataSource apkSigningBlock) throws IOException {
        // FORMAT:
        // uint64:  size (excluding this field)
        // repeated ID-value pairs:
        //     uint64:           size (excluding this field)
        //     uint32:           ID
        //     (size - 4) bytes: value
        // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
        // uint64:  size (same as the one above)
        // uint128: magic
        long apkSigningBlockSize = apkSigningBlock.size();
        if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) {
            throw new IllegalArgumentException(
                    "APK signing block size out of range: " + apkSigningBlockSize);
        }
        // Remove the header and footer from the signing block to iterate over only the repeated
        // ID-value pairs.
        ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8,
                (int) apkSigningBlock.size() - 32);
        apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
        List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>();
        while (apkSigningBlockBuffer.hasRemaining()) {
            long blockLength = apkSigningBlockBuffer.getLong();
            if (blockLength > Integer.MAX_VALUE || blockLength < 4) {
                throw new IllegalArgumentException(
                        "Block index " + (signatureBlocks.size() + 1) + " size out of range: "
                                + blockLength);
            }
            int blockId = apkSigningBlockBuffer.getInt();
            // Since the block ID has already been read from the signature block read the next
            // blockLength - 4 bytes as the value.
            byte[] blockValue = new byte[(int) blockLength - 4];
            apkSigningBlockBuffer.get(blockValue);
            signatureBlocks.add(Pair.of(blockValue, blockId));
        }
        return signatureBlocks;
    }

    /**
     * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code
     * List} of {@code Pair} instances where the first element is a {@code List} of {@link
     * X509Certificate}s and the second element is a byte array of the individual signer's block.
     *
     * <p>This method supports any signature block that adheres to the following format up to the
     * signing certificate(s):
     * <pre>
     * * length-prefixed sequence of length-prefixed signers
     *   * length-prefixed signed data
     *     * 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).
     * </pre>
     *
     * <p>Note, this is a convenience method to obtain any signers from an existing signature block;
     * the signature of each signer will not be verified.
     *
     * @throws ApkFormatException if an error is encountered while parsing the provided {@code
     * signatureBlock}
     * @throws CertificateException if the signing certificate(s) within an individual signer block
     * cannot be parsed
     */
    public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners(
            byte[] signatureBlock) throws ApkFormatException, CertificateException {
        ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock);
        signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
        ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer);
        List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>();
        while (signersBuffer.hasRemaining()) {
            // Parse the next signer block, save all of its bytes for the resulting List, and
            // rewind the buffer to allow the signing certificate(s) to be parsed.
            ByteBuffer signer = getLengthPrefixedSlice(signersBuffer);
            byte[] signerBytes = new byte[signer.remaining()];
            signer.get(signerBytes);
            signer.rewind();

            ByteBuffer signedData = getLengthPrefixedSlice(signer);
            // The first length prefixed slice is the sequence of digests which are not required
            // when obtaining the signing certificate(s).
            getLengthPrefixedSlice(signedData);
            ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData);
            List<X509Certificate> certificates = new ArrayList<>();
            while (certificatesBuffer.hasRemaining()) {
                int certLength = certificatesBuffer.getInt();
                byte[] certBytes = new byte[certLength];
                if (certLength > certificatesBuffer.remaining()) {
                    throw new IllegalArgumentException(
                            "Cert index " + (certificates.size() + 1) + " under signer index "
                                    + (signers.size() + 1) + " size out of range: " + certLength);
                }
                certificatesBuffer.get(certBytes);
                GuaranteedEncodedFormX509Certificate signerCert =
                        new GuaranteedEncodedFormX509Certificate(
                                X509CertificateUtils.generateCertificate(certBytes), certBytes);
                certificates.add(signerCert);
            }
            signers.add(Pair.of(certificates, signerBytes));
        }
        return signers;
    }

    /**
     * Computes the digests of the given APK components according to the algorithms specified in the
     * given SignerConfigs.
     *
     * @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 NoSuchAlgorithmException if a required cryptographic algorithm implementation is
     *         missing
     * @throws SignatureException if an error occurs when computing digests of generating
     *         signatures
     */
    public static Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>>
            computeContentDigests(
                    RunnablesExecutor executor,
                    DataSource beforeCentralDir,
                    DataSource centralDir,
                    DataSource eocd,
                    List<SignerConfig> signerConfigs)
                            throws IOException, NoSuchAlgorithmException, 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());
            }
        }

        // Compute digests of APK contents.
        Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
        try {
            contentDigests =
                    computeContentDigests(
                            executor,
                            contentDigestAlgorithms,
                            beforeCentralDir,
                            centralDir,
                            eocd);
        } 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 Pair.of(signerConfigs, contentDigests);
    }

    /**
     * Returns the subset of signatures which are expected to be verified by at least one Android
     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
     * guaranteed to contain at least one signature.
     *
     * <p>Each Android platform version typically verifies exactly one signature from the provided
     * {@code signatures} set. This method returns the set of these signatures collected over all
     * requested platform versions. As a result, the result may contain more than one signature.
     *
     * @throws NoSupportedSignaturesException if no supported signatures were
     *         found for an Android platform version in the range.
     */
    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
            List<T> signatures, int minSdkVersion, int maxSdkVersion)
            throws NoSupportedSignaturesException {
        return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
    }

    /**
     * Returns the subset of signatures which are expected to be verified by at least one Android
     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
     * guaranteed to contain at least one signature.
     *
     * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
     * signature within the signing block using the standard JCA.
     *
     * <p>Each Android platform version typically verifies exactly one signature from the provided
     * {@code signatures} set. This method returns the set of these signatures collected over all
     * requested platform versions. As a result, the result may contain more than one signature.
     *
     * @throws NoSupportedSignaturesException if no supported signatures were
     *         found for an Android platform version in the range.
     */
    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
            List<T> signatures, int minSdkVersion, int maxSdkVersion,
            boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException {
        try {
            return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion,
                    maxSdkVersion, onlyRequireJcaSupport);
        } catch (NoApkSupportedSignaturesException e) {
            throw new NoSupportedSignaturesException(e.getMessage());
        }
    }

    public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException {
        public NoSupportedSignaturesException(String message) {
            super(message);
        }
    }

    public static class SignatureNotFoundException extends Exception {
        private static final long serialVersionUID = 1L;

        public SignatureNotFoundException(String message) {
            super(message);
        }

        public SignatureNotFoundException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    /**
     * uses the SignatureAlgorithms in the provided signerConfig to sign the provided data
     *
     * @return list of signature algorithm IDs and their corresponding signatures over the data.
     */
    public static List<Pair<Integer, byte[]>> generateSignaturesOverData(
            SignerConfig signerConfig, byte[] data)
                    throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
        List<Pair<Integer, byte[]>> signatures =
                new ArrayList<>(signerConfig.signatureAlgorithms.size());
        PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
            Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
            String jcaSignatureAlgorithm = sigAlgAndParams.getFirst();
            AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond();

            byte[] signatureBytes;
            try {
                signatureBytes =
                        SignerEngineFactory.getImplementation(
                                        signerConfig.keyConfig,
                                        jcaSignatureAlgorithm,
                                        jcaSignatureAlgorithmParams)
                                .sign(data);
            } catch (InvalidKeyException e) {
                throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
            } catch (InvalidAlgorithmParameterException | SignatureException e) {
                throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
            }

            try {
                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
                signature.initVerify(publicKey);
                if (jcaSignatureAlgorithmParams != null) {
                    signature.setParameter(jcaSignatureAlgorithmParams);
                }
                signature.update(data);
                if (!signature.verify(signatureBytes)) {
                    throw new SignatureException("Failed to verify generated "
                            + jcaSignatureAlgorithm
                            + " signature using public key from certificate");
                }
            } catch (InvalidKeyException e) {
                throw new InvalidKeyException(
                        "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
                                + " public key from certificate", e);
            } catch (InvalidAlgorithmParameterException | SignatureException e) {
                throw new SignatureException(
                        "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
                                + " public key from certificate", e);
            }

            signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes));
        }
        return signatures;
    }

    /**
     * Wrap the signature according to CMS PKCS #7 RFC 5652.
     * The high-level simplified structure is as follows:
     * // ContentInfo
     *     //   digestAlgorithm
     *     //   SignedData
     *     //     bag of certificates
     *     //     SignerInfo
     *     //       signing cert issuer and serial number (for locating the cert in the above bag)
     *     //       digestAlgorithm
     *     //       signatureAlgorithm
     *     //       signature
     *
     * @throws Asn1EncodingException if the ASN.1 structure could not be encoded
     */
    public static byte[] generatePkcs7DerEncodedMessage(
            byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts,
            AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId)
            throws Asn1EncodingException, CertificateEncodingException {
        SignerInfo signerInfo = new SignerInfo();
        signerInfo.version = 1;
        X509Certificate signingCert = signerCerts.get(0);
        X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
        signerInfo.sid =
                new SignerIdentifier(
                        new IssuerAndSerialNumber(
                                new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
                                signingCert.getSerialNumber()));

        signerInfo.digestAlgorithm = digestAlgorithmId;
        signerInfo.signatureAlgorithm = signatureAlgorithmId;
        signerInfo.signature = ByteBuffer.wrap(signatureBytes);

        SignedData signedData = new SignedData();
        signedData.certificates = new ArrayList<>(signerCerts.size());
        for (X509Certificate cert : signerCerts) {
            signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
        }
        signedData.version = 1;
        signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
        signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
        // If data is not null, data will be embedded as is in the result -- an attached pcsk7
        signedData.encapContentInfo.content = data;
        signedData.signerInfos = Collections.singletonList(signerInfo);
        ContentInfo contentInfo = new ContentInfo();
        contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
        contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
        return Asn1DerEncoder.encode(contentInfo);
    }

    /**
     * Picks the correct v2/v3 digest for v4 signature verification.
     *
     * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils.
     */
    public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) {
        for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) {
            if (contentDigests.containsKey(algo)) {
                return contentDigests.get(algo);
            }
        }
        return null;
    }

    /** Signer configuration. */
    public static class SignerConfig {
        /**
         * Private key.
         *
         * @deprecated all internal usage has migrated to use {@link #keyConfig}. This field is not
         *     removed so that compilation is not broken for clients referencing it, but using this
         *     field may lead to unexpected errors.
         */
        @Deprecated public PrivateKey privateKey;

        /** Signing key config. */
        public KeyConfig keyConfig;

        /**
         * Certificates, with the first certificate containing the public key corresponding to
         * {@link #keyConfig}.
         */
        public List<X509Certificate> certificates;

        /**
         * List of signature algorithms with which to sign.
         */
        public List<SignatureAlgorithm> signatureAlgorithms;

        public int minSdkVersion;
        public int maxSdkVersion;
        public boolean signerTargetsDevRelease;
        public SigningCertificateLineage signingCertificateLineage;
    }

    public static class Result extends ApkSigResult {
        public SigningCertificateLineage signingCertificateLineage = null;
        public final List<Result.SignerInfo> signers = new ArrayList<>();
        private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
        private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();

        public Result(int signatureSchemeVersion) {
            super(signatureSchemeVersion);
        }

        public boolean containsErrors() {
            if (!mErrors.isEmpty()) {
                return true;
            }
            if (!signers.isEmpty()) {
                for (Result.SignerInfo signer : signers) {
                    if (signer.containsErrors()) {
                        return true;
                    }
                }
            }
            return false;
        }

        public boolean containsWarnings() {
            if (!mWarnings.isEmpty()) {
                return true;
            }
            if (!signers.isEmpty()) {
                for (Result.SignerInfo signer : signers) {
                    if (signer.containsWarnings()) {
                        return true;
                    }
                }
            }
            return false;
        }

        public void addError(ApkVerifier.Issue msg, Object... parameters) {
            mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
        }

        public void addWarning(ApkVerifier.Issue msg, Object... parameters) {
            mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
        }

        @Override
        public List<ApkVerifier.IssueWithParams> getErrors() {
            return mErrors;
        }

        @Override
        public List<ApkVerifier.IssueWithParams> getWarnings() {
            return mWarnings;
        }

        public static class SignerInfo extends ApkSignerInfo {
            public List<ContentDigest> contentDigests = new ArrayList<>();
            public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
            public List<Signature> signatures = new ArrayList<>();
            public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
            public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
            public byte[] signedData;
            public int minSdkVersion;
            public int maxSdkVersion;
            public SigningCertificateLineage signingCertificateLineage;

            private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
            private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();

            public void addError(ApkVerifier.Issue msg, Object... parameters) {
                mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
            }

            public void addWarning(ApkVerifier.Issue msg, Object... parameters) {
                mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
            }

            public boolean containsErrors() {
                return !mErrors.isEmpty();
            }

            public boolean containsWarnings() {
                return !mWarnings.isEmpty();
            }

            public List<ApkVerifier.IssueWithParams> getErrors() {
                return mErrors;
            }

            public List<ApkVerifier.IssueWithParams> getWarnings() {
                return mWarnings;
            }

            public static class ContentDigest {
                private final int mSignatureAlgorithmId;
                private final byte[] mValue;

                public ContentDigest(int signatureAlgorithmId, byte[] value) {
                    mSignatureAlgorithmId  = signatureAlgorithmId;
                    mValue = value;
                }

                public int getSignatureAlgorithmId() {
                    return mSignatureAlgorithmId;
                }

                public byte[] getValue() {
                    return mValue;
                }
            }

            public static class Signature {
                private final int mAlgorithmId;
                private final byte[] mValue;

                public Signature(int algorithmId, byte[] value) {
                    mAlgorithmId  = algorithmId;
                    mValue = value;
                }

                public int getAlgorithmId() {
                    return mAlgorithmId;
                }

                public byte[] getValue() {
                    return mValue;
                }
            }

            public static class AdditionalAttribute {
                private final int mId;
                private final byte[] mValue;

                public AdditionalAttribute(int id, byte[] value) {
                    mId  = id;
                    mValue = value.clone();
                }

                public int getId() {
                    return mId;
                }

                public byte[] getValue() {
                    return mValue.clone();
                }
            }
        }
    }

    public static class SupportedSignature extends ApkSupportedSignature {
        public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
            super(algorithm, signature);
        }
    }

    public static class SigningSchemeBlockAndDigests {
        public final Pair<byte[], Integer> signingSchemeBlock;
        public final Map<ContentDigestAlgorithm, byte[]> digestInfo;

        public SigningSchemeBlockAndDigests(
                Pair<byte[], Integer> signingSchemeBlock,
                Map<ContentDigestAlgorithm, byte[]> digestInfo) {
            this.signingSchemeBlock = signingSchemeBlock;
            this.digestInfo = digestInfo;
        }
    }
}
