Android Recovery OTA流程分析

提示:Android Recovery OTA流程分析,(此文中需留意代码中中文注释亦或是数字标示的流程顺序,有助于读懂流程,此文章也并非对流程阐述十分详细,但重要环节都有的)


前言

        非A/B升级,就是传统意义的Recovery升级。bootloader通过读取misc分区引导进入recovery,通过读/cache/recovery/command中的指令,来执行OTA逻辑,完成后清除misc分区重启设备,从而正常启动到新版本。

一、Framework层OTA逻辑

时序图如下:

涉及的主要函数代码如下:

1、verifyPackage()校验OTA包

frameworks/base/core/java/android/os/RecoverySystem.verifyPackage()

(verifyPackage()、installPackage()、rebootWipeUserData()、rebootWipeCache())

public static void verifyPackage(File packageFile,
                                     ProgressListener listener,
                                     File deviceCertsZipFile)
        throws IOException, GeneralSecurityException {
        final long fileLen = packageFile.length();

        final RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
        //01:获取OTA包
        try {
            final long startTimeMillis = System.currentTimeMillis();
            if (listener != null) {
                listener.onProgress(0);
            }
            //02:校验OTA包尾部6个字节的中间两个字节是否为0xff,与signapk.jar逻辑相同
            raf.seek(fileLen - 6);
            byte[] footer = new byte[6];
            raf.readFully(footer);

            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
                throw new SignatureException("no signature in file (no footer)");
            }
            //获取commentSize和signatureStart
            final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
            final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);

            byte[] eocd = new byte[commentSize + 22];
            raf.seek(fileLen - (commentSize + 22));
            raf.readFully(eocd);

            // Check that we have found the start of the
            // end-of-central-directory record.
            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
                throw new SignatureException("no signature in file (bad footer)");
            }
             //03:检查EOCD(end-of-central-directory)核心标识
            for (int i = 4; i < eocd.length-3; ++i) {
                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
                    throw new SignatureException("EOCD marker found after start of EOCD");
                }
            }

            // Parse the signature
            PKCS7 block =
                new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));

            // Take the first certificate from the signature (packages
            // should contain only one). 从OT包中获取证书
            X509Certificate[] certificates = block.getCertificates();
            if (certificates == null || certificates.length == 0) {
                throw new SignatureException("signature contains no certificates");
            }
            X509Certificate cert = certificates[0];
            PublicKey signatureKey = cert.getPublicKey();

            SignerInfo[] signerInfos = block.getSignerInfos();
            if (signerInfos == null || signerInfos.length == 0) {
                throw new SignatureException("signature contains no signedData");
            }
            SignerInfo signerInfo = signerInfos[0];

            // Check that the public key of the certificate contained
            // in the package equals one of our trusted public keys.
            /**04:检查signatureKey是否在我们信任的证书list之中,并对比公钥信息
             *DEFAULT_KEYSTORE("/system/etc/security/otacerts.zip")
             *这个文件就是在sign_target_files_apks.py中通过指定-O选项时更新的。
             *假如没有指定新的签名目录,那么使用原生的testkey作为密钥。
             *所以从校验可以看出,至少对ota整包的签名的公钥信息必须要与待OTA升级的system/etc/otacerts.zip中的公钥信息是要一致的,、
             *否则将在校验时出错.相当于CA的一个作用证明该公钥是有效的,才会继续使用公钥去计算出OTA包中的摘要*/
            boolean verified = false;
            HashSet<X509Certificate> trusted = getTrustedCerts(
                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
            for (X509Certificate c : trusted) {
                if (c.getPublicKey().equals(signatureKey)) {
                    verified = true;
                    break;
                }
            }
            if (!verified) {
                throw new SignatureException("signature doesn't match any trusted key");
            }

            // The signature cert matches a trusted key.  Now verify that
            // the digest in the cert matches the actual file data.
            //05:block.verify是进行摘要对比
            raf.seek(0);
            final ProgressListener listenerForInner = listener;
            SignerInfo verifyResult = block.verify(signerInfo, new InputStream() {
                // The signature covers all of the OTA package except the
                // archive comment and its 2-byte length.
                long toRead = fileLen - commentSize - 2;
                long soFar = 0;

                int lastPercent = 0;
                long lastPublishTime = startTimeMillis;

                @Override
                public int read() throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public int read(byte[] b, int off, int len) throws IOException {
                    if (soFar >= toRead) {
                        return -1;
                    }
                    if (Thread.currentThread().isInterrupted()) {
                        return -1;
                    }

                    int size = len;
                    if (soFar + size > toRead) {
                        size = (int)(toRead - soFar);
                    }
                    int read = raf.read(b, off, size);
                    soFar += read;

                    if (listenerForInner != null) {
                        long now = System.currentTimeMillis();
                        int p = (int)(soFar * 100 / toRead);
                        if (p > lastPercent &&
                            now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
                            lastPercent = p;
                            lastPublishTime = now;
                            listenerForInner.onProgress(lastPercent);
                        }
                    }

                    return read;
                }
            });

            final boolean interrupted = Thread.interrupted();
            if (listener != null) {
                listener.onProgress(100);
            }

            if (interrupted) {
                throw new SignatureException("verification was interrupted");
            }

            if (verifyResult == null) {
                throw new SignatureException("signature digest verification failed");
            }
        } finally {
            raf.close();
        }

        // Additionally verify the package compatibility.
        if (!readAndVerifyPackageCompatibilityEntry(packageFile)) {
            throw new SignatureException("package compatibility verification failed");
        }
    }

补充:SingApk.java(签名使用,可主要看下signWholeFile()。  此部分跟OTA包签名相关,后面验签与此处会有呼应,可以自行研究下。

      
/*
 * Copyright (C) 2008 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.signapk;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.conscrypt.OpenSSLProvider;

import com.android.apksig.ApkSignerEngine;
import com.android.apksig.DefaultApkSignerEngine;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.Hints;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.apk.MinSdkVersionException;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.zip.ZipFormatException;

import java.io.Console;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;

import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

/**
 * HISTORICAL NOTE:
 *
 * Prior to the keylimepie release, SignApk ignored the signature
 * algorithm specified in the certificate and always used SHA1withRSA.
 *
 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
 * the signature algorithm in the certificate to select which to use
 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
 *
 * Because there are old keys still in use whose certificate actually
 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
 * for compatibility with older releases.  This can be changed by
 * altering the getAlgorithm() function below.
 */


/**
 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
 * SHA-256 (see historical note). The tool can additionally sign APKs using
 * APK Signature Scheme v2.
 */
class SignApk {
    private static final String OTACERT_NAME = "META-INF/com/android/otacert";

    /**
     * Extensible data block/field header ID used for storing information about alignment of
     * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
     * 4.5 Extensible data fields.
     */
    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;

    /**
     * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
     * entries.
     */
    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;

    // bitmasks for which hash algorithms we need the manifest to include.
    private static final int USE_SHA1 = 1;
    private static final int USE_SHA256 = 2;

    /**
     * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
     * for signing an OTA update package using the private key corresponding to the provided
     * certificate.
     */
    private static int getDigestAlgorithmForOta(X509Certificate cert) {
        String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
        if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
            // see "HISTORICAL NOTE" above.
            return USE_SHA1;
        } else if (sigAlg.startsWith("SHA256WITH")) {
            return USE_SHA256;
        } else {
            throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
                                               "\" in cert [" + cert.getSubjectDN());
        }
    }

    /**
     * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
     * update package using the private key corresponding to the provided certificate and the
     * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
     */
    private static String getJcaSignatureAlgorithmForOta(
            X509Certificate cert, int hash) {
        String sigAlgDigestPrefix;
        switch (hash) {
            case USE_SHA1:
                sigAlgDigestPrefix = "SHA1";
                break;
            case USE_SHA256:
                sigAlgDigestPrefix = "SHA256";
                break;
            default:
                throw new IllegalArgumentException("Unknown hash ID: " + hash);
        }

        String keyAlgorithm = cert.getPublicKey().getAlgorithm();
        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
            return sigAlgDigestPrefix + "withRSA";
        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
            return sigAlgDigestPrefix + "withECDSA";
        } else {
            throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
        }
    }

    private static X509Certificate readPublicKey(File file)
        throws IOException, GeneralSecurityException {
        FileInputStream input = new FileInputStream(file);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            return (X509Certificate) cf.generateCertificate(input);
        } finally {
            input.close();
        }
    }

    /**
     * If a console doesn't exist, reads the password from stdin
     * If a console exists, reads the password from console and returns it as a string.
     *
     * @param keyFileName Name of the file containing the private key.  Used to prompt the user.
     */
    private static char[] readPassword(String keyFileName) {
        Console console;
        if ((console = System.console()) == null) {
            System.out.print(
                "Enter password for " + keyFileName + " (password will not be hidden): ");
            System.out.flush();
            BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
            try {
                String result = stdin.readLine();
                return result == null ? null : result.toCharArray();
            } catch (IOException ex) {
                return null;
            }
        } else {
            return console.readPassword("[%s]", "Enter password for " + keyFileName);
        }
    }

    /**
     * Decrypt an encrypted PKCS#8 format private key.
     *
     * Based on ghstark's post on Aug 6, 2006 at
     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
     *
     * @param encryptedPrivateKey The raw data of the private key
     * @param keyFile The file containing the private key
     */
    private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
        throws GeneralSecurityException {
        EncryptedPrivateKeyInfo epkInfo;
        try {
            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
        } catch (IOException ex) {
            // Probably not an encrypted key.
            return null;
        }

        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
        Key key = skFactory.generateSecret(new PBEKeySpec(readPassword(keyFile.getPath())));
        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());

        try {
            return epkInfo.getKeySpec(cipher);
        } catch (InvalidKeySpecException ex) {
            System.err.println("signapk: Password for " + keyFile + " may be bad.");
            throw ex;
        }
    }

    /** Read a PKCS#8 format private key. */
    private static PrivateKey readPrivateKey(File file)
        throws IOException, GeneralSecurityException {
        DataInputStream input = new DataInputStream(new FileInputStream(file));
        try {
            byte[] bytes = new byte[(int) file.length()];
            input.read(bytes);

            /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
            PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
            if (spec == null) {
                spec = new PKCS8EncodedKeySpec(bytes);
            }

            /*
             * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
             * OID and use that to construct a KeyFactory.
             */
            PrivateKeyInfo pki;
            try (ASN1InputStream bIn =
                    new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
                pki = PrivateKeyInfo.getInstance(bIn.readObject());
            }
            String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();

            return KeyFactory.getInstance(algOid).generatePrivate(spec);
        } finally {
            input.close();
        }
    }

    private static KeyStore createKeyStore(String keyStoreName, String keyStorePin) throws
            CertificateException,
            IOException,
            KeyStoreException,
            NoSuchAlgorithmException {
        KeyStore keyStore = KeyStore.getInstance(keyStoreName);
        keyStore.load(null, keyStorePin == null ? null : keyStorePin.toCharArray());
        return keyStore;
    }

    /** Get a PKCS#11 private key from keyStore */
    private static PrivateKey loadPrivateKeyFromKeyStore(
            final KeyStore keyStore, final String keyName)
            throws CertificateException, KeyStoreException, NoSuchAlgorithmException,
                    UnrecoverableKeyException, UnrecoverableEntryException {
        final Key key = keyStore.getKey(keyName, readPassword(keyName));
        final PrivateKeyEntry privateKeyEntry = (PrivateKeyEntry) keyStore.getEntry(keyName, null);
        if (privateKeyEntry == null) {
        throw new Error(
            "Key "
                + keyName
                + " not found in the token provided by PKCS11 library!");
        }
        return privateKeyEntry.getPrivateKey();
    }

    /**
     * Add a copy of the public key to the archive; this should
     * exactly match one of the files in
     * /system/etc/security/otacerts.zip on the device.  (The same
     * cert can be extracted from the OTA update package's signature
     * block but this is much easier to get at.)
     */
    private static void addOtacert(JarOutputStream outputJar,
                                   File publicKeyFile,
                                   long timestamp)
        throws IOException {

        JarEntry je = new JarEntry(OTACERT_NAME);
        je.setTime(timestamp);
        outputJar.putNextEntry(je);
        FileInputStream input = new FileInputStream(publicKeyFile);
        byte[] b = new byte[4096];
        int read;
        while ((read = input.read(b)) != -1) {
            outputJar.write(b, 0, read);
        }
        input.close();
    }


    /** Sign data and write the digital signature to 'out'. */
    private static void writeSignatureBlock(
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
        OutputStream out)
        throws IOException,
               CertificateEncodingException,
               OperatorCreationException,
               CMSException {
        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
        certList.add(publicKey);
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        ContentSigner signer =
                new JcaContentSignerBuilder(
                        getJcaSignatureAlgorithmForOta(publicKey, hash))
                        .build(privateKey);
        gen.addSignerInfoGenerator(
            new JcaSignerInfoGeneratorBuilder(
                new JcaDigestCalculatorProviderBuilder()
                .build())
            .setDirectSignature(true)
            .build(signer, publicKey));
        gen.addCertificates(certs);
        CMSSignedData sigData = gen.generate(data, false);

        try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
            DEROutputStream dos = new DEROutputStream(out);
            dos.writeObject(asn1.readObject());
        }
    }

    /**
     * Adds ZIP entries which represent the v1 signature (JAR signature scheme).
     */
    private static void addV1Signature(
            ApkSignerEngine apkSigner,
            ApkSignerEngine.OutputJarSignatureRequest v1Signature,
            JarOutputStream out,
            long timestamp) throws IOException {
        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry
                : v1Signature.getAdditionalJarEntries()) {
            String entryName = entry.getName();
            JarEntry outEntry = new JarEntry(entryName);
            outEntry.setTime(timestamp);
            out.putNextEntry(outEntry);
            byte[] entryData = entry.getData();
            out.write(entryData);
            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                    apkSigner.outputJarEntry(entryName);
            if (inspectEntryRequest != null) {
                inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length);
                inspectEntryRequest.done();
            }
        }
    }

    /**
     * Copy all JAR entries from input to output. We set the modification times in the output to a
     * fixed time, so as to reduce variation in the output file and make incremental OTAs more
     * efficient.
     */
    private static void copyFiles(
            JarFile in,
            Pattern ignoredFilenamePattern,
            ApkSignerEngine apkSigner,
            JarOutputStream out,
            CountingOutputStream outCounter,
            long timestamp,
            int defaultAlignment) throws IOException {
        byte[] buffer = new byte[4096];
        int num;

        List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(in);
        ArrayList<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();

        ArrayList<String> names = new ArrayList<String>();
        for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) {
            JarEntry entry = e.nextElement();
            if (entry.isDirectory()) {
                continue;
            }
            String entryName = entry.getName();
            if ((ignoredFilenamePattern != null)
                    && (ignoredFilenamePattern.matcher(entryName).matches())) {
                continue;
            }
            if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
                continue;  // We regenerate it below.
            }
            names.add(entryName);
        }
        Collections.sort(names);

        boolean firstEntry = true;
        long offset = 0L;

        // We do the copy in two passes -- first copying all the
        // entries that are STORED, then copying all the entries that
        // have any other compression flag (which in practice means
        // DEFLATED).  This groups all the stored entries together at
        // the start of the file and makes it easier to do alignment
        // on them (since only stored entries are aligned).

        List<String> remainingNames = new ArrayList<>(names.size());
        for (String name : names) {
            JarEntry inEntry = in.getJarEntry(name);
            if (inEntry.getMethod() != JarEntry.STORED) {
                // Defer outputting this entry until we're ready to output compressed entries.
                remainingNames.add(name);
                continue;
            }

            if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
                continue;
            }

            // Preserve the STORED method of the input entry.
            JarEntry outEntry = new JarEntry(inEntry);
            outEntry.setTime(timestamp);
            // Discard comment and extra fields of this entry to
            // simplify alignment logic below and for consistency with
            // how compressed entries are handled later.
            outEntry.setComment(null);
            outEntry.setExtra(null);

            int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
            // Alignment of the entry's data is achieved by adding a data block to the entry's Local
            // File Header extra field. The data block contains information about the alignment
            // value and the necessary padding bytes (0x00) to achieve the alignment.  This works
            // because the entry's data will be located immediately after the extra field.
            // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format
            // of the extra field.

            // 'offset' is the offset into the file at which we expect the entry's data to begin.
            // This is the value we need to make a multiple of 'alignment'.
            offset += JarFile.LOCHDR + outEntry.getName().length();
            if (firstEntry) {
                // The first entry in a jar file has an extra field of four bytes that you can't get
                // rid of; any extra data you specify in the JarEntry is appended to these forced
                // four bytes.  This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000.
                // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540
                // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619.
                offset += 4;
                firstEntry = false;
            }
            int extraPaddingSizeBytes = 0;
            if (alignment > 0) {
                long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
                extraPaddingSizeBytes =
                        (alignment - (int) (paddingStartOffset % alignment)) % alignment;
            }
            byte[] extra =
                    new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes];
            ByteBuffer extraBuf = ByteBuffer.wrap(extra);
            extraBuf.order(ByteOrder.LITTLE_ENDIAN);
            extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID
            extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size
            extraBuf.putShort((short) alignment);
            outEntry.setExtra(extra);
            offset += extra.length;

            long entryHeaderStart = outCounter.getWrittenBytes();
            out.putNextEntry(outEntry);
            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                    (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
            DataSink entryDataSink =
                    (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;

            long entryDataStart = outCounter.getWrittenBytes();
            try (InputStream data = in.getInputStream(inEntry)) {
                while ((num = data.read(buffer)) > 0) {
                    out.write(buffer, 0, num);
                    if (entryDataSink != null) {
                        entryDataSink.consume(buffer, 0, num);
                    }
                    offset += num;
                }
            }
            out.closeEntry();
            out.flush();
            if (inspectEntryRequest != null) {
                inspectEntryRequest.done();
            }

            if (pinPatterns != null) {
                boolean pinFileHeader = false;
                for (Hints.PatternWithRange pinPattern : pinPatterns) {
                    if (!pinPattern.matcher(name).matches()) {
                        continue;
                    }
                    Hints.ByteRange dataRange =
                        new Hints.ByteRange(
                            entryDataStart,
                            outCounter.getWrittenBytes());
                    Hints.ByteRange pinRange =
                        pinPattern.ClampToAbsoluteByteRange(dataRange);
                    if (pinRange != null) {
                        pinFileHeader = true;
                        pinByteRanges.add(pinRange);
                    }
                }
                if (pinFileHeader) {
                    pinByteRanges.add(new Hints.ByteRange(entryHeaderStart,
                                                          entryDataStart));
                }
            }
        }

        // Copy all the non-STORED entries.  We don't attempt to
        // maintain the 'offset' variable past this point; we don't do
        // alignment on these entries.

        for (String name : remainingNames) {
            JarEntry inEntry = in.getJarEntry(name);
            if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
                continue;
            }

            // Create a new entry so that the compressed len is recomputed.
            JarEntry outEntry = new JarEntry(name);
            outEntry.setTime(timestamp);
            long entryHeaderStart = outCounter.getWrittenBytes();
            out.putNextEntry(outEntry);
            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                    (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
            DataSink entryDataSink =
                    (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;

            long entryDataStart = outCounter.getWrittenBytes();
            InputStream data = in.getInputStream(inEntry);
            while ((num = data.read(buffer)) > 0) {
                out.write(buffer, 0, num);
                if (entryDataSink != null) {
                    entryDataSink.consume(buffer, 0, num);
                }
            }
            out.closeEntry();
            out.flush();
            if (inspectEntryRequest != null) {
                inspectEntryRequest.done();
            }

            if (pinPatterns != null) {
                boolean pinFileHeader = false;
                for (Hints.PatternWithRange pinPattern : pinPatterns) {
                    if (!pinPattern.matcher(name).matches()) {
                        continue;
                    }
                    Hints.ByteRange dataRange =
                        new Hints.ByteRange(
                            entryDataStart,
                            outCounter.getWrittenBytes());
                    Hints.ByteRange pinRange =
                        pinPattern.ClampToAbsoluteByteRange(dataRange);
                    if (pinRange != null) {
                        pinFileHeader = true;
                        pinByteRanges.add(pinRange);
                    }
                }
                if (pinFileHeader) {
                    pinByteRanges.add(new Hints.ByteRange(entryHeaderStart,
                                                          entryDataStart));
                }
            }
        }

        if (pinByteRanges != null) {
            // Cover central directory
            pinByteRanges.add(
                new Hints.ByteRange(outCounter.getWrittenBytes(),
                                    Long.MAX_VALUE));
            addPinByteRanges(out, pinByteRanges, timestamp);
        }
    }

    private static List<Hints.PatternWithRange> extractPinPatterns(JarFile in) throws IOException {
        ZipEntry pinMetaEntry = in.getEntry(Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
        if (pinMetaEntry == null) {
            return null;
        }
        InputStream pinMetaStream = in.getInputStream(pinMetaEntry);
        byte[] patternBlob = new byte[(int) pinMetaEntry.getSize()];
        pinMetaStream.read(patternBlob);
        return Hints.parsePinPatterns(patternBlob);
    }

    private static void addPinByteRanges(JarOutputStream outputJar,
                                         ArrayList<Hints.ByteRange> pinByteRanges,
                                         long timestamp) throws IOException {
        JarEntry je = new JarEntry(Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME);
        je.setTime(timestamp);
        outputJar.putNextEntry(je);
        outputJar.write(Hints.encodeByteRangeList(pinByteRanges));
    }

    private static boolean shouldOutputApkEntry(
            ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)
                    throws IOException {
        if (apkSigner == null) {
            return true;
        }

        ApkSignerEngine.InputJarEntryInstructions instructions =
                apkSigner.inputJarEntry(inEntry.getName());
        ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                instructions.getInspectJarEntryRequest();
        if (inspectEntryRequest != null) {
            provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf);
        }
        switch (instructions.getOutputPolicy()) {
            case OUTPUT:
                return true;
            case SKIP:
            case OUTPUT_BY_ENGINE:
                return false;
            default:
                throw new RuntimeException(
                        "Unsupported output policy: " + instructions.getOutputPolicy());
        }
    }

    private static void provideJarEntry(
            JarFile jarFile,
            JarEntry jarEntry,
            ApkSignerEngine.InspectJarEntryRequest request,
            byte[] tmpbuf) throws IOException {
        DataSink dataSink = request.getDataSink();
        try (InputStream in = jarFile.getInputStream(jarEntry)) {
            int chunkSize;
            while ((chunkSize = in.read(tmpbuf)) > 0) {
                dataSink.consume(tmpbuf, 0, chunkSize);
            }
            request.done();
        }
    }

    /**
     * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
     * relative to start of file or {@code 0} if alignment of this entry's data is not important.
     */
    private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
        if (defaultAlignment <= 0) {
            return 0;
        }

        if (entryName.endsWith(".so")) {
            // Align .so contents to memory page boundary to enable memory-mapped
            // execution.
            return 4096;
        } else {
            return defaultAlignment;
        }
    }

    private static class WholeFileSignerOutputStream extends FilterOutputStream {
        private boolean closing = false;
        private ByteArrayOutputStream footer = new ByteArrayOutputStream();
        private OutputStream tee;

        public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
            super(out);
            this.tee = tee;
        }

        public void notifyClosing() {
            closing = true;
        }

        public void finish() throws IOException {
            closing = false;

            byte[] data = footer.toByteArray();
            if (data.length < 2)
                throw new IOException("Less than two bytes written to footer");
            write(data, 0, data.length - 2);
        }

        public byte[] getTail() {
            return footer.toByteArray();
        }

        @Override
        public void write(byte[] b) throws IOException {
            write(b, 0, b.length);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (closing) {
                // if the jar is about to close, save the footer that will be written
                footer.write(b, off, len);
            }
            else {
                // write to both output streams. out is the CMSTypedData signer and tee is the file.
                out.write(b, off, len);
                tee.write(b, off, len);
            }
        }

        @Override
        public void write(int b) throws IOException {
            if (closing) {
                // if the jar is about to close, save the footer that will be written
                footer.write(b);
            }
            else {
                // write to both output streams. out is the CMSTypedData signer and tee is the file.
                out.write(b);
                tee.write(b);
            }
        }
    }

    private static class CMSSigner implements CMSTypedData {
        private final JarFile inputJar;
        private final File publicKeyFile;
        private final X509Certificate publicKey;
        private final PrivateKey privateKey;
        private final int hash;
        private final long timestamp;
        private final OutputStream outputStream;
        private final ASN1ObjectIdentifier type;
        private WholeFileSignerOutputStream signer;

        // Files matching this pattern are not copied to the output.
        private static final Pattern STRIP_PATTERN =
                Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
                        + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");

        public CMSSigner(JarFile inputJar, File publicKeyFile,
                         X509Certificate publicKey, PrivateKey privateKey, int hash,
                         long timestamp, OutputStream outputStream) {
            this.inputJar = inputJar;
            this.publicKeyFile = publicKeyFile;
            this.publicKey = publicKey;
            this.privateKey = privateKey;
            this.hash = hash;
            this.timestamp = timestamp;
            this.outputStream = outputStream;
            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
        }

        /**
         * This should actually return byte[] or something similar, but nothing
         * actually checks it currently.
         */
        @Override
        public Object getContent() {
            return this;
        }

        @Override
        public ASN1ObjectIdentifier getContentType() {
            return type;
        }

        @Override
        public void write(OutputStream out) throws IOException {
            try {
                signer = new WholeFileSignerOutputStream(out, outputStream);
                CountingOutputStream outputJarCounter = new CountingOutputStream(signer);
                JarOutputStream outputJar = new JarOutputStream(outputJarCounter);

                copyFiles(inputJar, STRIP_PATTERN, null, outputJar,
                          outputJarCounter, timestamp, 0);
                addOtacert(outputJar, publicKeyFile, timestamp);

                signer.notifyClosing();
                outputJar.close();
                signer.finish();
            }
            catch (Exception e) {
                throw new IOException(e);
            }
        }

        public void writeSignatureBlock(ByteArrayOutputStream temp)
            throws IOException,
                   CertificateEncodingException,
                   OperatorCreationException,
                   CMSException {
            SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp);
        }

        public WholeFileSignerOutputStream getSigner() {
            return signer;
        }
    }

    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
                                      X509Certificate publicKey, PrivateKey privateKey,
                                      int hash, long timestamp,
                                      OutputStream outputStream) throws Exception {
        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
                publicKey, privateKey, hash, timestamp, outputStream);

        ByteArrayOutputStream temp = new ByteArrayOutputStream();

        // put a readable message and a null char at the start of the
        // archive comment, so that tools that display the comment
        // (hopefully) show something sensible.
        // TODO: anything more useful we can put in this message?
        byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8);
        temp.write(message);
        temp.write(0);

        cmsOut.writeSignatureBlock(temp);

        byte[] zipData = cmsOut.getSigner().getTail();

        // For a zip with no archive comment, the
        // end-of-central-directory record will be 22 bytes long, so
        // we expect to find the EOCD marker 22 bytes from the end.
        if (zipData[zipData.length-22] != 0x50 ||
            zipData[zipData.length-21] != 0x4b ||
            zipData[zipData.length-20] != 0x05 ||
            zipData[zipData.length-19] != 0x06) {
            throw new IllegalArgumentException("zip data already has an archive comment");
        }

        int total_size = temp.size() + 6;
        if (total_size > 0xffff) {
            throw new IllegalArgumentException("signature is too big for ZIP file comment");
        }
        // signature starts this many bytes from the end of the file
        int signature_start = total_size - message.length - 1;
        temp.write(signature_start & 0xff);
        temp.write((signature_start >> 8) & 0xff);
        // Why the 0xff bytes?  In a zip file with no archive comment,
        // bytes [-6:-2] of the file are the little-endian offset from
        // the start of the file to the central directory.  So for the
        // two high bytes to be 0xff 0xff, the archive would have to
        // be nearly 4GB in size.  So it's unlikely that a real
        // commentless archive would have 0xffs here, and lets us tell
        // an old signed archive from a new one.
        temp.write(0xff);
        temp.write(0xff);
        temp.write(total_size & 0xff);
        temp.write((total_size >> 8) & 0xff);
        temp.flush();

        // Signature verification checks that the EOCD header is the
        // last such sequence in the file (to avoid minzip finding a
        // fake EOCD appended after the signature in its scan).  The
        // odds of producing this sequence by chance are very low, but
        // let's catch it here if it does.
        byte[] b = temp.toByteArray();
        for (int i = 0; i < b.length-3; ++i) {
            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
                throw new IllegalArgumentException("found spurious EOCD header at " + i);
            }
        }

        outputStream.write(total_size & 0xff);
        outputStream.write((total_size >> 8) & 0xff);
        temp.writeTo(outputStream);
    }

    /**
     * Tries to load a JSE Provider by class name. This is for custom PrivateKey
     * types that might be stored in PKCS#11-like storage.
     */
    private static void loadProviderIfNecessary(String providerClassName) {
        if (providerClassName == null) {
            return;
        }

        final Class<?> klass;
        try {
            final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
            if (sysLoader != null) {
                klass = sysLoader.loadClass(providerClassName);
            } else {
                klass = Class.forName(providerClassName);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            System.exit(1);
            return;
        }

        Constructor<?> constructor = null;
        for (Constructor<?> c : klass.getConstructors()) {
            if (c.getParameterTypes().length == 0) {
                constructor = c;
                break;
            }
        }
        if (constructor == null) {
            System.err.println("No zero-arg constructor found for " + providerClassName);
            System.exit(1);
            return;
        }

        final Object o;
        try {
            o = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
            return;
        }
        if (!(o instanceof Provider)) {
            System.err.println("Not a Provider class: " + providerClassName);
            System.exit(1);
        }

        Security.insertProviderAt((Provider) o, 1);
    }

    private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
            PrivateKey[] privateKeys, X509Certificate[] certificates) {
        if (privateKeys.length != certificates.length) {
            throw new IllegalArgumentException(
                    "The number of private keys must match the number of certificates: "
                            + privateKeys.length + " vs" + certificates.length);
        }
        List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
        String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
        for (int i = 0; i < privateKeys.length; i++) {
            String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
            DefaultApkSignerEngine.SignerConfig signerConfig =
                    new DefaultApkSignerEngine.SignerConfig.Builder(
                            signerName,
                            privateKeys[i],
                            Collections.singletonList(certificates[i]))
                            .build();
            signerConfigs.add(signerConfig);
        }
        return signerConfigs;
    }

    private static class ZipSections {
        ByteBuffer beforeCentralDir;
        ByteBuffer centralDir;
        ByteBuffer eocd;
    }

    private static ZipSections findMainZipSections(ByteBuffer apk)
            throws IOException, ZipFormatException {
        apk.slice();
        ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
        long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
        long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
        long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
        long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
        if (centralDirEndOffset != eocdStartOffset) {
            throw new ZipFormatException(
                    "ZIP Central Directory is not immediately followed by End of Central Directory"
                            + ". CD end: " + centralDirEndOffset
                            + ", EoCD start: " + eocdStartOffset);
        }
        apk.position(0);
        apk.limit((int) centralDirStartOffset);
        ByteBuffer beforeCentralDir = apk.slice();

        apk.position((int) centralDirStartOffset);
        apk.limit((int) centralDirEndOffset);
        ByteBuffer centralDir = apk.slice();

        apk.position((int) eocdStartOffset);
        apk.limit(apk.capacity());
        ByteBuffer eocd = apk.slice();

        apk.position(0);
        apk.limit(apk.capacity());

        ZipSections result = new ZipSections();
        result.beforeCentralDir = beforeCentralDir;
        result.centralDir = centralDir;
        result.eocd = eocd;
        return result;
    }

    /**
     * Returns the API Level corresponding to the APK's minSdkVersion.
     *
     * @throws MinSdkVersionException if the API Level cannot be determined from the APK.
     */
    private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException {
        JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml");
        if (manifestEntry == null) {
            throw new MinSdkVersionException("No AndroidManifest.xml in APK");
        }
        byte[] manifestBytes;
        try {
            try (InputStream manifestIn = apk.getInputStream(manifestEntry)) {
                manifestBytes = toByteArray(manifestIn);
            }
        } catch (IOException e) {
            throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e);
        }
        return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes));
    }

    private static byte[] toByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buf = new byte[65536];
        int chunkSize;
        while ((chunkSize = in.read(buf)) != -1) {
            result.write(buf, 0, chunkSize);
        }
        return result.toByteArray();
    }

    private static void usage() {
        System.err.println("Usage: signapk [-w] " +
                           "[-a <alignment>] " +
                           "[--align-file-size] " +
                           "[-providerClass <className>] " +
                           "[-loadPrivateKeysFromKeyStore <keyStoreName>]" +
                           "[-keyStorePin <pin>]" +
                           "[--min-sdk-version <n>] " +
                           "[--disable-v2] " +
                           "[--enable-v4] " +
                           "publickey.x509[.pem] privatekey.pk8 " +
                           "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
                           "input.jar output.jar [output-v4-file]");
        System.exit(2);
    }

    public static void main(String[] args) {
        if (args.length < 4) usage();

        // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
        // the standard or Bouncy Castle ones.
        Security.insertProviderAt(new OpenSSLProvider(), 1);
        // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
        // DSA which may still be needed.
        // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
        Security.addProvider(new BouncyCastleProvider());

        boolean signWholeFile = false;
        String providerClass = null;
        String keyStoreName = null;
        String keyStorePin = null;
        int alignment = 4;
        boolean alignFileSize = false;
        Integer minSdkVersionOverride = null;
        boolean signUsingApkSignatureSchemeV2 = true;
        boolean signUsingApkSignatureSchemeV4 = false;
        SigningCertificateLineage certLineage = null;
        Integer rotationMinSdkVersion = null;

        int argstart = 0;
        while (argstart < args.length && args[argstart].startsWith("-")) {
            if ("-w".equals(args[argstart])) {
                signWholeFile = true;
                ++argstart;
            } else if ("-providerClass".equals(args[argstart])) {
                if (argstart + 1 >= args.length) {
                    usage();
                }
                providerClass = args[++argstart];
                ++argstart;
            } else if ("-loadPrivateKeysFromKeyStore".equals(args[argstart])) {
                if (argstart + 1 >= args.length) {
                    usage();
                }
                keyStoreName = args[++argstart];
                ++argstart;
            } else if ("-keyStorePin".equals(args[argstart])) {
                if (argstart + 1 >= args.length) {
                    usage();
                }
                keyStorePin = args[++argstart];
                ++argstart;
            } else if ("-a".equals(args[argstart])) {
                alignment = Integer.parseInt(args[++argstart]);
                ++argstart;
            } else if ("--align-file-size".equals(args[argstart])) {
                alignFileSize = true;
                ++argstart;
            } else if ("--min-sdk-version".equals(args[argstart])) {
                String minSdkVersionString = args[++argstart];
                try {
                    minSdkVersionOverride = Integer.parseInt(minSdkVersionString);
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException(
                            "--min-sdk-version must be a decimal number: " + minSdkVersionString);
                }
                ++argstart;
            } else if ("--disable-v2".equals(args[argstart])) {
                signUsingApkSignatureSchemeV2 = false;
                ++argstart;
            } else if ("--enable-v4".equals(args[argstart])) {
                signUsingApkSignatureSchemeV4 = true;
                ++argstart;
            } else if ("--lineage".equals(args[argstart])) {
                File lineageFile = new File(args[++argstart]);
                try {
                    certLineage = SigningCertificateLineage.readFromFile(lineageFile);
                } catch (Exception e) {
                    throw new IllegalArgumentException(
                            "Error reading lineage file: " + e.getMessage());
                }
                ++argstart;
            } else if ("--rotation-min-sdk-version".equals(args[argstart])) {
                String rotationMinSdkVersionString = args[++argstart];
                try {
                    rotationMinSdkVersion = Integer.parseInt(rotationMinSdkVersionString);
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException(
                            "--rotation-min-sdk-version must be a decimal number: " + rotationMinSdkVersionString);
                }
                ++argstart;
            } else {
                usage();
            }
        }

        int numArgsExcludeV4FilePath;
        if (signUsingApkSignatureSchemeV4) {
            numArgsExcludeV4FilePath = args.length - 1;
        } else {
            numArgsExcludeV4FilePath = args.length;
        }
        if ((numArgsExcludeV4FilePath - argstart) % 2 == 1) usage();
        int numKeys = ((numArgsExcludeV4FilePath - argstart) / 2) - 1;
        if (signWholeFile && numKeys > 1) {
            System.err.println("Only one key may be used with -w.");
            System.exit(2);
        }

        loadProviderIfNecessary(providerClass);

        String inputFilename = args[numArgsExcludeV4FilePath - 2];
        String outputFilename = args[numArgsExcludeV4FilePath - 1];
        String outputV4Filename = "";
        if (signUsingApkSignatureSchemeV4) {
            outputV4Filename = args[args.length - 1];
        }

        JarFile inputJar = null;
        FileOutputStream outputFile = null;

        try {
            File firstPublicKeyFile = new File(args[argstart+0]);

            X509Certificate[] publicKey = new X509Certificate[numKeys];
            try {
                for (int i = 0; i < numKeys; ++i) {
                    int argNum = argstart + i*2;
                    publicKey[i] = readPublicKey(new File(args[argNum]));
                }
            } catch (IllegalArgumentException e) {
                System.err.println(e);
                System.exit(1);
            }

            // Set all ZIP file timestamps to Jan 1 2009 00:00:00.
            long timestamp = 1230768000000L;
            // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
            // timestamp using the current timezone. We thus adjust the milliseconds since epoch
            // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
            timestamp -= TimeZone.getDefault().getOffset(timestamp);
            KeyStore keyStore = null;
            if (keyStoreName != null) {
                keyStore = createKeyStore(keyStoreName, keyStorePin);
            }
            PrivateKey[] privateKey = new PrivateKey[numKeys];
            for (int i = 0; i < numKeys; ++i) {
                int argNum = argstart + i*2 + 1;
                if (keyStore == null) {
                    privateKey[i] = readPrivateKey(new File(args[argNum]));
                } else {
                    final String keyAlias = args[argNum];
                    privateKey[i] = loadPrivateKeyFromKeyStore(keyStore, keyAlias);
                }
            }
            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.

            outputFile = new FileOutputStream(outputFilename);

            // NOTE: Signing currently recompresses any compressed entries using Deflate (default
            // compression level for OTA update files and maximum compession level for APKs).
            if (signWholeFile) {
                int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]);
                signWholeFile(inputJar, firstPublicKeyFile,
                        publicKey[0], privateKey[0], digestAlgorithm,
                        timestamp,
                        outputFile);
            } else {
                // Determine the value to use as minSdkVersion of the APK being signed
                int minSdkVersion;
                if (minSdkVersionOverride != null) {
                    minSdkVersion = minSdkVersionOverride;
                } else {
                    try {
                        minSdkVersion = getMinSdkVersion(inputJar);
                    } catch (MinSdkVersionException e) {
                        throw new IllegalArgumentException(
                                "Cannot detect minSdkVersion. Use --min-sdk-version to override",
                                e);
                    }
                }

                DefaultApkSignerEngine.Builder builder = new DefaultApkSignerEngine.Builder(
                    createSignerConfigs(privateKey, publicKey), minSdkVersion)
                    .setV1SigningEnabled(true)
                    .setV2SigningEnabled(signUsingApkSignatureSchemeV2)
                    .setOtherSignersSignaturesPreserved(false)
                    .setCreatedBy("1.0 (Android SignApk)");

                if (certLineage != null) {
                   builder = builder.setSigningCertificateLineage(certLineage);
                }

                if (rotationMinSdkVersion != null) {
                   builder = builder.setMinSdkVersionForRotation(rotationMinSdkVersion);
                }

                try (ApkSignerEngine apkSigner = builder.build()) {
                    // We don't preserve the input APK's APK Signing Block (which contains v2
                    // signatures)
                    apkSigner.inputApkSigningBlock(null);

                    // Build the output APK in memory, by copying input APK's ZIP entries across
                    // and then signing the output APK.
                    ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
                    CountingOutputStream outputJarCounter =
                            new CountingOutputStream(v1SignedApkBuf);
                    JarOutputStream outputJar = new JarOutputStream(outputJarCounter);
                    // Use maximum compression for compressed entries because the APK lives forever
                    // on the system partition.
                    outputJar.setLevel(9);
                    copyFiles(inputJar, null, apkSigner, outputJar,
                              outputJarCounter, timestamp, alignment);
                    ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
                            apkSigner.outputJarEntries();
                    if (addV1SignatureRequest != null) {
                        addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
                        addV1SignatureRequest.done();
                    }
                    outputJar.close();
                    ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
                    v1SignedApkBuf.reset();
                    ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};

                    ZipSections zipSections = findMainZipSections(v1SignedApk);

                    ByteBuffer eocd = ByteBuffer.allocate(zipSections.eocd.remaining());
                    eocd.put(zipSections.eocd);
                    eocd.flip();
                    eocd.order(ByteOrder.LITTLE_ENDIAN);
                    // This loop is supposed to be iterated twice at most.
                    // The second pass is to align the file size after amending EOCD comments
                    // with assumption that re-generated signing block would be the same size.
                    while (true) {
                        ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest =
                                apkSigner.outputZipSections2(
                                        DataSources.asDataSource(zipSections.beforeCentralDir),
                                        DataSources.asDataSource(zipSections.centralDir),
                                        DataSources.asDataSource(eocd));
                        if (addV2SignatureRequest == null) break;

                        // Need to insert the returned APK Signing Block before ZIP Central
                        // Directory.
                        int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock();
                        byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
                        // Because the APK Signing Block is inserted before the Central Directory,
                        // we need to adjust accordingly the offset of Central Directory inside the
                        // ZIP End of Central Directory (EoCD) record.
                        ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
                        modifiedEocd.put(eocd);
                        modifiedEocd.flip();
                        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
                        ApkUtils.setZipEocdCentralDirectoryOffset(
                                modifiedEocd,
                                zipSections.beforeCentralDir.remaining() + padding +
                                apkSigningBlock.length);
                        outputChunks =
                                new ByteBuffer[] {
                                        zipSections.beforeCentralDir,
                                        ByteBuffer.allocate(padding),
                                        ByteBuffer.wrap(apkSigningBlock),
                                        zipSections.centralDir,
                                        modifiedEocd};
                        addV2SignatureRequest.done();

                        // Exit the loop if we don't need to align the file size
                        if (!alignFileSize || alignment < 2) {
                            break;
                        }

                        // Calculate the file size
                        eocd = modifiedEocd;
                        int fileSize = 0;
                        for (ByteBuffer buf : outputChunks) {
                            fileSize += buf.remaining();
                        }
                        // Exit the loop because the file size is aligned.
                        if (fileSize % alignment == 0) {
                            break;
                        }
                        // Pad EOCD comment to align the file size.
                        int commentLen = alignment - fileSize % alignment;
                        modifiedEocd = ByteBuffer.allocate(eocd.remaining() + commentLen);
                        modifiedEocd.put(eocd);
                        modifiedEocd.rewind();
                        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
                        ApkUtils.updateZipEocdCommentLen(modifiedEocd);
                        // Since V2 signing block should cover modified EOCD,
                        // re-iterate the loop with modified EOCD.
                        eocd = modifiedEocd;
                    }

                    // This assumes outputChunks are array-backed. To avoid this assumption, the
                    // code could be rewritten to use FileChannel.
                    for (ByteBuffer outputChunk : outputChunks) {
                        outputFile.write(
                                outputChunk.array(),
                                outputChunk.arrayOffset() + outputChunk.position(),
                                outputChunk.remaining());
                        outputChunk.position(outputChunk.limit());
                    }

                    outputFile.close();
                    outputFile = null;
                    apkSigner.outputDone();

                    if (signUsingApkSignatureSchemeV4) {
                        final DataSource outputApkIn = DataSources.asDataSource(
                                new RandomAccessFile(new File(outputFilename), "r"));
                        final File outputV4File =  new File(outputV4Filename);
                        apkSigner.signV4(outputApkIn, outputV4File, false /* ignore failures */);
                    }
                }

                return;
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        } finally {
            try {
                if (inputJar != null) inputJar.close();
                if (outputFile != null) outputFile.close();
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    }
}

2、installPackage()安装OTA包

BCB :Bootloader Control Block, BCB是Bootloader和recovery的通信接口,

储存在misc分区。

frameworks/base/core/java/android/os/RecoverySystem.installPackage()

     /**
     * If the package hasn't been processed (i.e. uncrypt'd), set up
     * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the
     * reboot.
     *
     * @param context      the Context to use
     * @param packageFile  the update package to install.  Must be on a
     * partition mountable by recovery.
     * @param processed    if the package has been processed (uncrypt'd).
     *
     * @throws IOException if writing the recovery command file fails, or if
     * the reboot itself fails.
     *
     * @hide
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.RECOVERY)
    public static void installPackage(Context context, File packageFile, boolean processed)
            throws IOException {
        synchronized (sRequestLock) {
            //删除LOG文件("/cache/recovery/log")
            LOG_FILE.delete();
            // Must delete the file in case it was created by system server.
            // 删除之前的uncrypt文件(cache/recovery/uncrypt_file) 文件保存着升级包路径
            UNCRYPT_PACKAGE_FILE.delete();

            String filename = packageFile.getCanonicalPath();
            Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");

            // If the package name ends with "_s.zip", it's a security update.
            boolean securityUpdate = filename.endsWith("_s.zip");

            // If the package is on the /data partition, the package needs to
            // be processed (i.e. uncrypt'd). The caller specifies if that has
            // been done in 'processed' parameter.
            //详看补充描述
            if (filename.startsWith("/data/")) {
                if (processed) {
                    if (!BLOCK_MAP_FILE.exists()) {
                        Log.e(TAG, "Package claimed to have been processed but failed to find "
                                + "the block map file.");
                        throw new IOException("Failed to find block map file");
                    }
                } else {
                    FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
                    try {
                        //OTA包路径保存到uncrypt_file
                        uncryptFile.write(filename + "\n");
                    } finally {
                        uncryptFile.close();
                    }
                    // UNCRYPT_PACKAGE_FILE needs to be readable and writable
                    // by system server.
                    if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
                            || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
                        Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
                    }
                    //删除/cache/recovery/block.map(recovery使用此文件来标识OTA包在/data分区上的位置(block)。
                    //块映射文件是通过取消加密生成的。)
                    BLOCK_MAP_FILE.delete();
                }

                // If the package is on the /data partition, use the block map
                // file as the package name instead.
                filename = "@/cache/recovery/block.map";
            }
            //向bootloader control block写入命名
            final String filenameArg = "--update_package=" + filename + "\n";
            final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
            final String securityArg = "--security\n";

            String command = filenameArg + localeArg;
            if (securityUpdate) {
                command += securityArg;
            }

            RecoverySystem rs = (RecoverySystem) context.getSystemService(
                    Context.RECOVERY_SERVICE);
            if (!rs.setupBcb(command)) {
                throw new IOException("Setup BCB failed");
            }
            try {
                if (!rs.allocateSpaceForUpdate(packageFile)) {
                    rs.clearBcb();
                    throw new IOException("Failed to allocate space for update "
                            + packageFile.getAbsolutePath());
                }
            } catch (RemoteException e) {
                rs.clearBcb();
                e.rethrowAsRuntimeException();
            }

            // Having set up the BCB (bootloader control block), go ahead and reboot
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            String reason = PowerManager.REBOOT_RECOVERY_UPDATE;

            // On TV, reboot quiescently if the screen is off
            if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
                DisplayManager dm = context.getSystemService(DisplayManager.class);
                if (dm.getDisplay(DEFAULT_DISPLAY).getState() != Display.STATE_ON) {
                    reason += ",quiescent";
                }
            }
            //此处重启设备到recovery
            pm.reboot(reason);

            throw new IOException("Reboot failed (no permissions?)");
        }
    }
    
     /**
     * Talks to RecoverySystemService via Binder to set up the BCB.
     */
    private boolean setupBcb(String command) {
        try {
            return mService.setupBcb(command);
        } catch (RemoteException unused) {
        }
        return false;
    }

3、setuoBcb函数解析:

frameworks/base/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java中。

    private RecoverySystemService(Context context) {
        this(new Injector(context));
    }

    @VisibleForTesting
    RecoverySystemService(Injector injector) {
        mInjector = injector;
        mContext = injector.getContext();
    }
    ......
    @Override // Binder call
    public boolean setupBcb(String command) {
        if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
        synchronized (sRequestLock) {
            return setupOrClearBcb(true, command);
        }
    }
    
    
    private boolean setupOrClearBcb(boolean isSetup, String command) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);

        final boolean available = checkAndWaitForUncryptService();
        if (!available) {
            Slog.e(TAG, "uncrypt service is unavailable.");
            return false;
        }
        /**
        *设置ctl.start属性,启动setup-bcb服务,isSetup传的是true,所以会启动setup-bcb服务
        */
        if (isSetup) {
            mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
        } else {
            mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
        }

        // Connect to the uncrypt service socket.
        UncryptSocket socket = mInjector.connectService();
        if (socket == null) {
            Slog.e(TAG, "Failed to connect to uncrypt socket");
            return false;
        }

        try {
            // Send the BCB commands if it's to setup BCB.
            //我们传入的command:--update_package=" + filename + "\n"
            if (isSetup) {
                socket.sendCommand(command);
            }

            // Read the status from the socket.
            int status = socket.getPercentageUncrypted();

            // Ack receipt of the status code. uncrypt waits for the ack so
            // the socket won't be destroyed before we receive the code.
            socket.sendAck();

            if (status == 100) {
                Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
                        + " bcb successfully finished.");
            } else {
                // Error in /system/bin/uncrypt.
                Slog.e(TAG, "uncrypt failed with status: " + status);
                return false;
            }
        } catch (IOException e) {
            Slog.e(TAG, "IOException when communicating with uncrypt:", e);
            return false;
        } finally {
            socket.close();
        }

        return true;
    }

二、Recover中的OTA逻辑

时序图如下:

涉及的主要函数代码逻辑如下:

1、recovery_main:main(),从bootloder引导分区后会判断misc的参数,如果是boot-recovery ,会驱动进入recovery模式。

bootloader/recovery/recovery_main:main()

int main(int argc, char** argv) {
  // We don't have logcat yet under recovery; so we'll print error on screen and log to stdout
  // (which is redirected to recovery.log) as we used to do.
  android::base::InitLogging(argv, &UiLogger);

  // Take last pmsg contents and rewrite it to the current pmsg session.
  static constexpr const char filter[] = "recovery/";
  // Do we need to rotate?
  bool do_rotate = false;

  __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &do_rotate);
  // Take action to refresh pmsg contents
  __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &do_rotate);

  time_t start = time(nullptr);

  // redirect_stdio should be called only in non-sideload mode. Otherwise we may have two logger
  // instances with different timestamps.
  redirect_stdio(Paths::Get().temporary_log_file().c_str());
  //加载分区表,定义在bootable/recovery/recovery_utils/roots.cpp中
  /**
   *void load_volume_table() {
   *  if (!ReadDefaultFstab(&fstab)) {
   *    LOG(ERROR) << "Failed to read default fstab";
   *    return;
   *  }
   * 
   *  fstab.emplace_back(FstabEntry{
   *      .blk_device = "ramdisk",
   *      .mount_point = "/tmp",
   *      .fs_type = "ramdisk",
   *      .length = 0,
   *  });
   * 
   *  mt_load_volume_table(&fstab);
   *
   *  std::cout << "recovery filesystem table" << std::endl << "=========================" << std::endl;
   *  for (size_t i = 0; i < fstab.size(); ++i) {
   *    const auto& entry = fstab[i];
   *    std::cout << "  " << i << " " << entry.mount_point << " "
   *              << " " << entry.fs_type << " " << entry.blk_device << " " << entry.length
   *              << std::endl;
   *  }
   *  std::cout << std::endl;
   *}
   */
  load_volume_table();

  std::string stage;
  std::vector<std::string> args = get_args(argc, argv, &stage);//获取recovery指令
  auto args_to_parse = StringVectorToNullTerminatedArray(args);

  static constexpr struct option OPTIONS[] = {
    { "fastboot", no_argument, nullptr, 0 },
    { "locale", required_argument, nullptr, 0 },
    { "reason", required_argument, nullptr, 0 },
    { "show_text", no_argument, nullptr, 't' },
    { nullptr, 0, nullptr, 0 },
  };

  bool show_text = false;
  bool fastboot = false;
  std::string locale;
  std::string reason;

  // The code here is only interested in the options that signal the intent to start fastbootd or
  // recovery. Unrecognized options are likely meant for recovery, which will be processed later in
  // start_recovery(). Suppress the warnings for such -- even if some flags were indeed invalid, the
  // code in start_recovery() will capture and report them.
  opterr = 0;

  int arg;
  int option_index;
  while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,
                            &option_index)) != -1) {
    switch (arg) {
      case 't':
        show_text = true;
        break;
      case 0: {
        std::string option = OPTIONS[option_index].name;
        if (option == "locale") {
          locale = optarg;
        } else if (option == "reason") {
          reason = optarg;
        } else if (option == "fastboot" &&
                   android::base::GetBoolProperty("ro.boot.dynamic_partitions", false)) {
          fastboot = true;
        }
        break;
      }
    }
  }
  optind = 1;
  opterr = 1;

  if (locale.empty()) {
    if (HasCache()) {
      locale = load_locale_from_cache();
    }

    if (locale.empty()) {
      locale = DEFAULT_LOCALE;
    }
  }

  static constexpr const char* kDefaultLibRecoveryUIExt = "librecovery_ui_ext.so";
  // Intentionally not calling dlclose(3) to avoid potential gotchas (e.g. `make_device` may have
  // handed out pointers to code or static [or thread-local] data and doesn't collect them all back
  // in on dlclose).
  void* librecovery_ui_ext = dlopen(kDefaultLibRecoveryUIExt, RTLD_NOW);

  using MakeDeviceType = decltype(&make_device);
  MakeDeviceType make_device_func = nullptr;
  if (librecovery_ui_ext == nullptr) {
    printf("Failed to dlopen %s: %s\n", kDefaultLibRecoveryUIExt, dlerror());
  } else {
    reinterpret_cast<void*&>(make_device_func) = dlsym(librecovery_ui_ext, "make_device");
    if (make_device_func == nullptr) {
      printf("Failed to dlsym make_device: %s\n", dlerror());
    }
  }

  Device* device;
  if (make_device_func == nullptr) {
    printf("Falling back to the default make_device() instead\n");
    device = make_device();
  } else {
    printf("Loading make_device from %s\n", kDefaultLibRecoveryUIExt);
    device = (*make_device_func)();
  }

  if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
    printf("Quiescent recovery mode.\n");
    device->ResetUI(new StubRecoveryUI());
  } else {
    if (!device->GetUI()->Init(locale)) {
      printf("Failed to initialize UI; using stub UI instead.\n");
      device->ResetUI(new StubRecoveryUI());
    }
  }

  BootState boot_state(reason, stage);  // recovery_main owns the state of boot.
  device->SetBootState(&boot_state);
  ui = device->GetUI();

  if (!HasCache()) {
    device->RemoveMenuItemForAction(Device::WIPE_CACHE);
  }

  if (!android::base::GetBoolProperty("ro.boot.dynamic_partitions", false)) {
    device->RemoveMenuItemForAction(Device::ENTER_FASTBOOT);
  }

  if (!IsRoDebuggable()) {
    device->RemoveMenuItemForAction(Device::ENTER_RESCUE);
  }

  ui->SetBackground(RecoveryUI::NONE);
  if (show_text) ui->ShowText(true);

  LOG(INFO) << "Starting recovery (pid " << getpid() << ") on " << ctime(&start);
  LOG(INFO) << "locale is [" << locale << "]";
  //"system/sepolicy/recovery.te" recovery的selinux权限相关
  auto sehandle = selinux_android_file_context_handle();
  selinux_android_set_sehandle(sehandle);
  if (!sehandle) {
    ui->Print("Warning: No file_contexts\n");
  }

  SetLoggingSehandle(sehandle);

  std::atomic<Device::BuiltinAction> action;
  std::thread listener_thread(ListenRecoverySocket, ui, std::ref(action));
  listener_thread.detach();

  while (true) {
    // We start adbd in recovery for the device with userdebug build or a unlocked bootloader.
    std::string usb_config =
        fastboot ? "fastboot" : IsRoDebuggable() || IsDeviceUnlocked() ? "adb" : "none";
    std::string usb_state = android::base::GetProperty("sys.usb.state", "none");
    if (fastboot) {
      device->PreFastboot();
    } else {
      device->PreRecovery();
    }
    if (usb_config != usb_state) {
      if (!SetUsbConfig("none")) {
        LOG(ERROR) << "Failed to clear USB config";
      }
      if (!SetUsbConfig(usb_config)) {
        LOG(ERROR) << "Failed to set USB config to " << usb_config;
      }
    }

    ui->SetEnableFastbootdLogo(fastboot);
    //start_recovery 开始进入升级流程
    auto ret = fastboot ? StartFastboot(device, args) : start_recovery(device, args);

    //此处为对升级结果的处理
    if (ret == Device::KEY_INTERRUPTED) {
      ret = action.exchange(ret);
      if (ret == Device::NO_ACTION) {
        continue;
      }
    }
    switch (ret) {
      case Device::SHUTDOWN:
        ui->Print("Shutting down...\n");
        Shutdown("userrequested,recovery");
        break;

      case Device::SHUTDOWN_FROM_FASTBOOT:
        ui->Print("Shutting down...\n");
        Shutdown("userrequested,fastboot");
        break;

      case Device::REBOOT_BOOTLOADER:
        ui->Print("Rebooting to bootloader...\n");
        Reboot("bootloader");
        break;

      case Device::REBOOT_FASTBOOT:
        ui->Print("Rebooting to recovery/fastboot...\n");
        Reboot("fastboot");
        break;

      case Device::REBOOT_RECOVERY:
        ui->Print("Rebooting to recovery...\n");
        Reboot("recovery");
        break;

      case Device::REBOOT_RESCUE: {
        // Not using `Reboot("rescue")`, as it requires matching support in kernel and/or
        // bootloader.
        bootloader_message boot = {};
        strlcpy(boot.command, "boot-rescue", sizeof(boot.command));
        std::string err;
        if (!write_bootloader_message(boot, &err)) {
          LOG(ERROR) << "Failed to write bootloader message: " << err;
          // Stay under recovery on failure.
          continue;
        }
        ui->Print("Rebooting to recovery/rescue...\n");
        Reboot("recovery");
        break;
      }

      case Device::ENTER_FASTBOOT:
        if (android::fs_mgr::LogicalPartitionsMapped()) {
          ui->Print("Partitions may be mounted - rebooting to enter fastboot.");
          Reboot("fastboot");
        } else {
          LOG(INFO) << "Entering fastboot";
          fastboot = true;
        }
        break;

      case Device::ENTER_RECOVERY:
        LOG(INFO) << "Entering recovery";
        fastboot = false;
        break;

      case Device::REBOOT:
        ui->Print("Rebooting...\n");
        Reboot("userrequested,recovery");
        break;

      case Device::REBOOT_FROM_FASTBOOT:
        ui->Print("Rebooting...\n");
        Reboot("userrequested,fastboot");
        break;

      default:
        ui->Print("Rebooting...\n");
        Reboot("unknown" + std::to_string(ret));
        break;
    }
  }

  // Should be unreachable.
  return EXIT_SUCCESS;
}

//省略代码。。。。。。

// Parses the command line argument from various sources; and reads the stage field from BCB.
// command line args come from, in decreasing precedence:
//   - the actual command line
//   - the bootloader control block (one per line, after "recovery")
//   - the contents of COMMAND_FILE (one per line)
static std::vector<std::string> get_args(const int argc, char** const argv, std::string* stage) {
  CHECK_GT(argc, 0);

  bootloader_message boot = {};
  std::string err;
  if (!read_bootloader_message(&boot, &err)) {
    LOG(ERROR) << err;
    // If fails, leave a zeroed bootloader_message.
    boot = {};
  }
  if (stage) {
    *stage = std::string(boot.stage);
  }

  std::string boot_command;
  if (boot.command[0] != 0) {
    if (memchr(boot.command, '\0', sizeof(boot.command))) {
      boot_command = std::string(boot.command);
    } else {
      boot_command = std::string(boot.command, sizeof(boot.command));
    }
    LOG(INFO) << "Boot command: " << boot_command;
  }

  if (boot.status[0] != 0) {
    std::string boot_status = std::string(boot.status, sizeof(boot.status));
    LOG(INFO) << "Boot status: " << boot_status;
  }

  std::vector<std::string> args(argv, argv + argc);

  // --- if arguments weren't supplied, look in the bootloader control block
  if (args.size() == 1) {
    boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
    std::string boot_recovery(boot.recovery);
    std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
    if (!tokens.empty() && tokens[0] == "recovery") {
      for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
        // Skip empty and '\0'-filled tokens.
        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
      }
      LOG(INFO) << "Got " << args.size() << " arguments from boot message";
    } else if (boot.recovery[0] != 0) {
      LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
    }
  }

  // --- if that doesn't work, try the command file (if we have /cache).
  if (args.size() == 1 && HasCache()) {
    std::string content;
    if (ensure_path_mounted(COMMAND_FILE) == 0 &&
        android::base::ReadFileToString(COMMAND_FILE, &content)) {
      std::vector<std::string> tokens = android::base::Split(content, "\n");
      // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
      // COMMAND_FILE doesn't use filename as the first argument).
      for (auto it = tokens.begin(); it != tokens.end(); it++) {
        // Skip empty and '\0'-filled tokens.
        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
      }
      LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
    }
  }

  // Write the arguments (excluding the filename in args[0]) back into the
  // bootloader control block. So the device will always boot into recovery to
  // finish the pending work, until FinishRecovery() is called.
  std::vector<std::string> options(args.cbegin() + 1, args.cend());
  if (!update_bootloader_message(options, &err)) {
    LOG(ERROR) << "Failed to set BCB message: " << err;
  }

  // Finally, if no arguments were specified, check whether we should boot
  // into fastboot or rescue mode.
  if (args.size() == 1 && boot_command == "boot-fastboot") {
    args.emplace_back("--fastboot");
  } else if (args.size() == 1 && boot_command == "boot-rescue") {
    args.emplace_back("--rescue");
  }

  return args;
}

2、start_recovery函数进入到之后的recovery.cpp

bootloader/recovery/recovery.cpp

Device::BuiltinAction start_recovery(Device* device, const std::vector<std::string>& args) {
  static constexpr struct option OPTIONS[] = {
    { "fastboot", no_argument, nullptr, 0 },
    { "install_with_fuse", no_argument, nullptr, 0 },
    { "just_exit", no_argument, nullptr, 'x' },
    { "locale", required_argument, nullptr, 0 },
    { "prompt_and_wipe_data", no_argument, nullptr, 0 },
    { "reason", required_argument, nullptr, 0 },
    { "rescue", no_argument, nullptr, 0 },
    { "retry_count", required_argument, nullptr, 0 },
    { "security", no_argument, nullptr, 0 },
    { "show_text", no_argument, nullptr, 't' },
    { "shutdown_after", no_argument, nullptr, 0 },
    { "sideload", no_argument, nullptr, 0 },
    { "sideload_auto_reboot", no_argument, nullptr, 0 },
    { "update_package", required_argument, nullptr, 0 },
    { "wipe_ab", no_argument, nullptr, 0 },
    { "wipe_cache", no_argument, nullptr, 0 },
    { "wipe_data", no_argument, nullptr, 0 },
    { "wipe_package_size", required_argument, nullptr, 0 },
    { nullptr, 0, nullptr, 0 },
  };

  const char* update_package = nullptr;
  bool install_with_fuse = false;  // memory map the update package by default.
  bool should_wipe_data = false;
  bool should_prompt_and_wipe_data = false;
  bool should_wipe_cache = false;
  bool should_wipe_ab = false;
  size_t wipe_package_size = 0;
  bool sideload = false;
  bool sideload_auto_reboot = false;
  bool rescue = false;
  bool just_exit = false;
  bool shutdown_after = false;
  int retry_count = 0;
  bool security_update = false;
  std::string locale;

  auto args_to_parse = StringVectorToNullTerminatedArray(args);

  int arg;
  int option_index;
  // Parse everything before the last element (which must be a nullptr). getopt_long(3) expects a
  // null-terminated char* array, but without counting null as an arg (i.e. argv[argc] should be
  // nullptr).
  while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,
                            &option_index)) != -1) {
    switch (arg) {
      case 't':
        // Handled in recovery_main.cpp
        break;
      case 'x':
        just_exit = true;
        break;
      case 0: {
        std::string option = OPTIONS[option_index].name;
        if (option == "install_with_fuse") {
          install_with_fuse = true;
        } else if (option == "locale" || option == "fastboot") {
          // Handled in recovery_main.cpp
        } else if (option == "prompt_and_wipe_data") {
          should_prompt_and_wipe_data = true;
        } else if (option == "reason") {
          reason = optarg;
        }else if (option == "rescue") {
          rescue = true;
        } else if (option == "retry_count") {
          android::base::ParseInt(optarg, &retry_count, 0);
        } else if (option == "security") {
          security_update = true;
        } else if (option == "sideload") {
          sideload = true;
        } else if (option == "sideload_auto_reboot") {
          sideload = true;
          sideload_auto_reboot = true;
        } else if (option == "shutdown_after") {
          shutdown_after = true;
        } else if (option == "update_package") {
          update_package = optarg;
        } else if (option == "wipe_ab") {
          should_wipe_ab = true;
        } else if (option == "wipe_cache") {
          should_wipe_cache = true;
        } else if (option == "wipe_data") {
          should_wipe_data = true;
        } else if (option == "wipe_package_size") {
          android::base::ParseUint(optarg, &wipe_package_size);
        }
        break;
      }
      case '?':
        LOG(ERROR) << "Invalid command argument";
        continue;
    }
  }
  optind = 1;

  printf("stage is [%s]\n", device->GetStage().value_or("").c_str());
  printf("reason is [%s]\n", device->GetReason().value_or("").c_str());

  auto ui = device->GetUI();

  // Set background string to "installing security update" for security update,
  // otherwise set it to "installing system update".
  ui->SetSystemUpdateText(security_update);

  int st_cur, st_max;
  if (!device->GetStage().has_value() &&
      sscanf(device->GetStage().value().c_str(), "%d/%d", &st_cur, &st_max) == 2) {
    ui->SetStage(st_cur, st_max);
  }

  std::vector<std::string> title_lines =
      android::base::Split(android::base::GetProperty("ro.build.fingerprint", ""), ":");
  title_lines.insert(std::begin(title_lines), "Android Recovery");
  ui->SetTitle(title_lines);

  ui->ResetKeyInterruptStatus();
  device->StartRecovery();

  printf("Command:");
  for (const auto& arg : args) {
    //Log中打印[    0.417046] Command: "/system/bin/recovery" "--update_package=@/cache/recovery/block.map" "--locale=en-US"
    printf(" \"%s\"", arg.c_str());
  }
  printf("\n\n");

  property_list(print_property, nullptr);
  printf("\n");

  InstallResult status = INSTALL_SUCCESS;
  // next_action indicates the next target to reboot into upon finishing the install. It could be
  // overridden to a different reboot target per user request.
  Device::BuiltinAction next_action = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;

  if (update_package != nullptr) {
    // It's not entirely true that we will modify the flash. But we want
    // to log the update attempt since update_package is non-NULL.
    save_current_log = true;
    //电量检查, Normally, the threshold is 40% without charger and 20% with charger
    if (int required_battery_level; retry_count == 0 && !IsBatteryOk(&required_battery_level)) {
      ui->Print("battery capacity is not enough for installing package: %d%% needed\n",
                required_battery_level);
      // Log the error code to last_install when installation skips due to low battery.
      log_failure_code(kLowBattery, update_package);
      status = INSTALL_SKIPPED;
    } else if (retry_count == 0 && bootreason_in_blocklist()) {
      // Skip update-on-reboot when bootreason is kernel_panic or similar
      ui->Print("bootreason is in the blocklist; skip OTA installation\n");
      log_failure_code(kBootreasonInBlocklist, update_package);
      status = INSTALL_SKIPPED;
    } else {
      // It's a fresh update. Initialize the retry_count in the BCB to 1; therefore we can later
      // identify the interrupted update due to unexpected reboots.
      if (retry_count == 0) {
        set_retry_bootloader_message(retry_count + 1, args);
      }

      bool should_use_fuse = false;
      if (!SetupPackageMount(update_package, &should_use_fuse)) {
        LOG(INFO) << "Failed to set up the package access, skipping installation";
        status = INSTALL_ERROR;
      } else if (install_with_fuse || should_use_fuse) {
        LOG(INFO) << "Installing package " << update_package << " with fuse";
        status = InstallWithFuseFromPath(update_package, ui);
      } else if (auto memory_package = Package::CreateMemoryPackage(
                     update_package,
                     std::bind(&RecoveryUI::SetProgress, ui, std::placeholders::_1));
                 memory_package != nullptr) {
        status = InstallPackage(memory_package.get(), update_package, should_wipe_cache,
                                retry_count, ui);
      } else {
        // We may fail to memory map the package on 32 bit builds for packages with 2GiB+ size.
        // In such cases, we will try to install the package with fuse. This is not the default
        // installation method because it introduces a layer of indirection from the kernel space.
        LOG(WARNING) << "Failed to memory map package " << update_package
                     << "; falling back to install with fuse";
        status = InstallWithFuseFromPath(update_package, ui);
      }
      if (status != INSTALL_SUCCESS) {
        ui->Print("Installation aborted.\n");

        // When I/O error or bspatch/imgpatch error happens, reboot and retry installation
        // RETRY_LIMIT times before we abandon this OTA update.
        static constexpr int RETRY_LIMIT = 4;
        if (status == INSTALL_RETRY && retry_count < RETRY_LIMIT) {
          copy_logs(save_current_log);
          retry_count += 1;
          set_retry_bootloader_message(retry_count, args);
          // Print retry count on screen.
          ui->Print("Retry attempt %d\n", retry_count);

          // Reboot back into recovery to retry the update.
          Reboot("recovery");
        }
        // If this is an eng or userdebug build, then automatically
        // turn the text display on if the script fails so the error
        // message is visible.
        if (IsRoDebuggable()) {
          ui->ShowText(true);
        }
      }
    }
  } else if (should_wipe_data) {
    save_current_log = true;
    bool convert_fbe = reason && strncmp(reason, "convert_fbe", strlen("convert_fbe")) == 0;
    if (!WipeData(device, convert_fbe)) {
      status = INSTALL_ERROR;
    }
  } else if (should_prompt_and_wipe_data) {
    // Trigger the logging to capture the cause, even if user chooses to not wipe data.
    save_current_log = true;

    ui->ShowText(true);
    ui->SetBackground(RecoveryUI::ERROR);
    status = prompt_and_wipe_data(device);
    if (status != INSTALL_KEY_INTERRUPTED) {
      ui->ShowText(false);
    }
  } else if (should_wipe_cache) {
    save_current_log = true;
    if (!WipeCache(ui, nullptr)) {
      status = INSTALL_ERROR;
    }
  } else if (should_wipe_ab) {
    if (!WipeAbDevice(device, wipe_package_size)) {
      status = INSTALL_ERROR;
    }
  } else if (sideload) {
    // 'adb reboot sideload' acts the same as user presses key combinations to enter the sideload
    // mode. When 'sideload-auto-reboot' is used, text display will NOT be turned on by default. And
    // it will reboot after sideload finishes even if there are errors. This is to enable automated
    // testing.
    save_current_log = true;
    if (!sideload_auto_reboot) {
      ui->ShowText(true);
    }
    status = ApplyFromAdb(device, false /* rescue_mode */, &next_action);
    ui->Print("\nInstall from ADB complete (status: %d).\n", status);
    if (sideload_auto_reboot) {
      status = INSTALL_REBOOT;
      ui->Print("Rebooting automatically.\n");
    }
  } else if (rescue) {
    save_current_log = true;
    status = ApplyFromAdb(device, true /* rescue_mode */, &next_action);
    ui->Print("\nInstall from ADB complete (status: %d).\n", status);
  } else if (!just_exit) {
    // If this is an eng or userdebug build, automatically turn on the text display if no command
    // is specified. Note that this should be called before setting the background to avoid
    // flickering the background image.
    if (IsRoDebuggable()) {
      ui->ShowText(true);
    }
    status = INSTALL_NONE;  // No command specified
    ui->SetBackground(RecoveryUI::NO_COMMAND);
  }

  if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
    ui->SetBackground(RecoveryUI::ERROR);
    if (!ui->IsTextVisible()) {
      sleep(5);
    }
  }

  // Determine the next action.
  //  - If the state is INSTALL_REBOOT, device will reboot into the target as specified in
  //    `next_action`.
  //  - If the recovery menu is visible, prompt and wait for commands.
  //  - If the state is INSTALL_NONE, wait for commands (e.g. in user build, one manually boots
  //    into recovery to sideload a package or to wipe the device).
  //  - In all other cases, reboot the device. Therefore, normal users will observe the device
  //    rebooting a) immediately upon successful finish (INSTALL_SUCCESS); or b) an "error" screen
  //    for 5s followed by an automatic reboot.
  if (status != INSTALL_REBOOT) {
    if (status == INSTALL_NONE || ui->IsTextVisible()) {
      auto temp = PromptAndWait(device, status);
      if (temp != Device::NO_ACTION) {
        next_action = temp;
      }
    }
  }

  // Save logs and clean up before rebooting or shutting down.
  FinishRecovery(ui);

  return next_action;
}

3、InstallPackage(),然后流程走到recovery/install/install.cpp:InstallPackage()

InstallResult InstallPackage(Package* package, const std::string_view package_id,
                             bool should_wipe_cache, int retry_count, RecoveryUI* ui) {
  auto start = std::chrono::system_clock::now();

  int start_temperature = GetMaxValueFromThermalZone();
  int max_temperature = start_temperature;

  InstallResult result;
  std::vector<std::string> log_buffer;

  ui->Print("Supported API: %d\n", kRecoveryApiVersion);

  ui->Print("Finding update package...\n");
  LOG(INFO) << "Update package id: " << package_id;
  if (!package) {
    log_buffer.push_back(android::base::StringPrintf("error: %d", kMapFileFailure));
    result = INSTALL_CORRUPT;
  } else if (setup_install_mounts() != 0) {
    LOG(ERROR) << "failed to set up expected mounts for install; aborting";
    result = INSTALL_ERROR;
  } else {
    bool updater_wipe_cache = false;
    result = VerifyAndInstallPackage(package, &updater_wipe_cache, &log_buffer, retry_count,
                                     &max_temperature, ui);
    should_wipe_cache = should_wipe_cache || updater_wipe_cache;
  }
  //省略代码。。。。。。
  return result;
}

//省略代码。。。。。。
static InstallResult VerifyAndInstallPackage(Package* package, bool* wipe_cache,
                                             std::vector<std::string>* log_buffer, int retry_count,
                                             int* max_temperature, RecoveryUI* ui) {
  ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
  // Give verification half the progress bar...
  ui->SetProgressType(RecoveryUI::DETERMINATE);
  ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);

  // Verify package.
  if (!verify_package(package, ui)) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kZipVerificationFailure));
    return INSTALL_CORRUPT;
  }

  // Verify and install the contents of the package.
  ui->Print("Installing update...\n");
  if (retry_count > 0) {
    ui->Print("Retry attempt: %d\n", retry_count);
  }
  ui->SetEnableReboot(false);
  auto result = TryUpdateBinary(package, wipe_cache, log_buffer, retry_count, max_temperature, ui);
  ui->SetEnableReboot(true);
  ui->Print("\n");

  return result;
}

//省略代码。。。。。。
bool verify_package(Package* package, RecoveryUI* ui) {
  static constexpr const char* CERTIFICATE_ZIP_FILE = "/system/etc/security/otacerts.zip";
  std::vector<Certificate> loaded_keys = LoadKeysFromZipfile(CERTIFICATE_ZIP_FILE);
  if (loaded_keys.empty()) {
    LOG(ERROR) << "Failed to load keys";
    return false;
  }
  LOG(INFO) << loaded_keys.size() << " key(s) loaded from " << CERTIFICATE_ZIP_FILE;

  // Verify package.
  ui->Print("Verifying update package...\n");
  auto t0 = std::chrono::system_clock::now();
  int err = verify_file(package, loaded_keys);
  std::chrono::duration<double> duration = std::chrono::system_clock::now() - t0;
  ui->Print("Update package verification took %.1f s (result %d).\n", duration.count(), err);
  if (err != VERIFY_SUCCESS) {
    LOG(ERROR) << "Signature verification failed";
    LOG(ERROR) << "error: " << kZipVerificationFailure;
    return false;
  }
  return true;
}

//省略代码。。。。。。
// If the package contains an update binary, extract it and run it.
static InstallResult TryUpdateBinary(Package* package, bool* wipe_cache,
                                     std::vector<std::string>* log_buffer, int retry_count,
                                     int* max_temperature, RecoveryUI* ui) {
  std::map<std::string, std::string> metadata;
  auto zip = package->GetZipArchiveHandle();
  if (!ReadMetadataFromPackage(zip, &metadata)) {
    LOG(ERROR) << "Failed to parse metadata in the zip file";
    return INSTALL_CORRUPT;
  }
  //是否为A/B升级(我们是recovery升级)
  bool package_is_ab = get_value(metadata, "ota-type") == OtaTypeToString(OtaType::AB);
  
//省略代码。。。。。。

  std::string package_path = package->GetPath();

  std::vector<std::string> args;
  if (auto setup_result =
          package_is_ab
              ? SetUpAbUpdateCommands(package_path, zip, pipe_write.get(), &args)
              : SetUpNonAbUpdateCommands(package_path, zip, retry_count, pipe_write.get(), &args);
      !setup_result) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure));
    return INSTALL_CORRUPT;
  }
  
  //fork子进程。其中的子进程主要负责执行binary,也就是执行安装命令脚本,
  //父进程负责接受子进程发送的命令去更新ui显示(显示当前的进度)。子父进程间通信依靠管道
  pid_t pid = fork();
  if (pid == -1) {
    PLOG(ERROR) << "Failed to fork update binary";
    log_buffer->push_back(android::base::StringPrintf("error: %d", kForkUpdateBinaryFailure));
    return INSTALL_ERROR;
  }

  if (pid == 0) {
    umask(022);
    pipe_read.reset();

    // Convert the std::string vector to a NULL-terminated char* vector suitable for execv.
    auto chr_args = StringVectorToNullTerminatedArray(args);
    execv(chr_args[0], chr_args.data());
    // We shouldn't use LOG/PLOG in the forked process, since they may cause the child process to
    // hang. This deadlock results from an improperly copied mutex in the ui functions.
    // (Bug: 34769056)
    fprintf(stdout, "E:Can't run %s (%s)\n", chr_args[0], strerror(errno));
    _exit(EXIT_FAILURE);
  }
  pipe_write.reset();

  std::atomic<bool> logger_finished(false);
  std::thread temperature_logger(log_max_temperature, max_temperature, std::ref(logger_finished));

  *wipe_cache = false;
  bool retry_update = false;

  char buffer[1024];
  FILE* from_child = android::base::Fdopen(std::move(pipe_read), "r");
  while (fgets(buffer, sizeof(buffer), from_child) != nullptr) {
    //。。。。。。
  return INSTALL_SUCCESS;
}

//省略代码。。。。。。。

bool SetUpNonAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int retry_count,
                              int status_fd, std::vector<std::string>* cmd) {
  CHECK(cmd != nullptr);

  // In non-A/B updates we extract the update binary from the package.
  static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
  ZipEntry64 binary_entry;
  if (FindEntry(zip, UPDATE_BINARY_NAME, &binary_entry) != 0) {
    LOG(ERROR) << "Failed to find update binary " << UPDATE_BINARY_NAME;
    return false;
  }

  const std::string binary_path = Paths::Get().temporary_update_binary();
  unlink(binary_path.c_str());
  android::base::unique_fd fd(
      open(binary_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0755));
  if (fd == -1) {
    PLOG(ERROR) << "Failed to create " << binary_path;
    return false;
  }

  if (auto error = ExtractEntryToFile(zip, &binary_entry, fd); error != 0) {
    LOG(ERROR) << "Failed to extract " << UPDATE_BINARY_NAME << ": " << ErrorCodeString(error);
    return false;
  }

  // When executing the update binary contained in the package, the arguments passed are:
  //   - the version number for this interface
  //   - an FD to which the program can write in order to update the progress bar.
  //   - the name of the package zip file.
  //   - an optional argument "retry" if this update is a retry of a failed update attempt.
  *cmd = {
    binary_path,
    std::to_string(kRecoveryApiVersion),
    std::to_string(status_fd),
    package,
  };
  if (retry_count > 0) {
    cmd->push_back("retry");
  }
  return true;
}

4、verify_file函数,开始校验OTA包的正确性。

recovery/install/verifier.cpp

int verify_file(VerifierInterface* package, const std::vector<Certificate>& keys) {
  CHECK(package);
  package->SetProgress(0.0);
#define FOOTER_SIZE 6
  //省略代码。。。。。。

  //校验OTA包尾部6个字节的中间两个字节是否为0xff(此处校验逻辑与RecoverySystem.verifyPackage基本一致)
  if (footer[2] != 0xff || footer[3] != 0xff) {
    LOG(ERROR) << "footer is wrong";
    return VERIFY_FAILURE;
  }

  //结尾格式以2字节`signature_start` 0xff 0xff 2字节`comment_size`结尾 
  size_t comment_size = footer[4] + (footer[5] << 8);,
  size_t signature_start = footer[0] + (footer[1] << 8);
  LOG(INFO) << "comment is " << comment_size << " bytes; signature is " << signature_start
            << " bytes from end";
  //比较
  if (signature_start > comment_size) {
    LOG(ERROR) << "signature start: " << signature_start
               << " is larger than comment size: " << comment_size;
    return VERIFY_FAILURE;
  }

  if (signature_start <= FOOTER_SIZE) {
    LOG(ERROR) << "Signature start is in the footer";
    return VERIFY_FAILURE;
  }

#define EOCD_HEADER_SIZE 22

  // The end-of-central-directory record is 22 bytes plus any comment length.
  size_t eocd_size = comment_size + EOCD_HEADER_SIZE;

  if (length < eocd_size) {
    LOG(ERROR) << "not big enough to contain EOCD";
    return VERIFY_FAILURE;
  }

  // Determine how much of the file is covered by the signature. This is everything except the
  // signature data and length, which includes all of the EOCD except for the comment length field
  // (2 bytes) and the comment data.
  uint64_t signed_len = length - eocd_size + EOCD_HEADER_SIZE - 2;

  uint8_t eocd[eocd_size];
  if (!package->ReadFullyAtOffset(eocd, eocd_size, length - eocd_size)) {
    LOG(ERROR) << "Failed to read EOCD of " << eocd_size << " bytes";
    return VERIFY_FAILURE;
  }

  // If this is really is the EOCD record, it will begin with the magic number $50 $4b $05 $06.
  if (eocd[0] != 0x50 || eocd[1] != 0x4b || eocd[2] != 0x05 || eocd[3] != 0x06) {
    LOG(ERROR) << "signature length doesn't match EOCD marker";
    return VERIFY_FAILURE;
  }

  for (size_t i = 4; i < eocd_size - 3; ++i) {
    if (eocd[i] == 0x50 && eocd[i + 1] == 0x4b && eocd[i + 2] == 0x05 && eocd[i + 3] == 0x06) {
      // If the sequence $50 $4b $05 $06 appears anywhere after the real one, libziparchive will
      // find the later (wrong) one, which could be exploitable. Fail the verification if this
      // sequence occurs anywhere after the real one.
      LOG(ERROR) << "EOCD marker occurs after start of EOCD";
      return VERIFY_FAILURE;
    }
  }

  bool need_sha1 = false;
  bool need_sha256 = false;
  for (const auto& key : keys) {
    switch (key.hash_len) {
      case SHA_DIGEST_LENGTH:
        need_sha1 = true;
        break;
      case SHA256_DIGEST_LENGTH:
        need_sha256 = true;
        break;
    }
  }

  SHA_CTX sha1_ctx;
  SHA256_CTX sha256_ctx;
  SHA1_Init(&sha1_ctx);
  SHA256_Init(&sha256_ctx);

  std::vector<HasherUpdateCallback> hashers;
  if (need_sha1) {
    hashers.emplace_back(
        std::bind(&SHA1_Update, &sha1_ctx, std::placeholders::_1, std::placeholders::_2));
  }
  if (need_sha256) {
    hashers.emplace_back(
        std::bind(&SHA256_Update, &sha256_ctx, std::placeholders::_1, std::placeholders::_2));
  }

  double frac = -1.0;
  uint64_t so_far = 0;
  while (so_far < signed_len) {
    // On a Nexus 5X, experiment showed 16MiB beat 1MiB by 6% faster for a 1196MiB full OTA and
    // 60% for an 89MiB incremental OTA. http://b/28135231.
    uint64_t read_size = std::min<uint64_t>(signed_len - so_far, 16 * MiB);
    package->UpdateHashAtOffset(hashers, so_far, read_size);
    so_far += read_size;

    double f = so_far / static_cast<double>(signed_len);
    if (f > frac + 0.02 || read_size == so_far) {
      package->SetProgress(f);
      frac = f;
    }
  }

  uint8_t sha1[SHA_DIGEST_LENGTH];
  SHA1_Final(sha1, &sha1_ctx);
  uint8_t sha256[SHA256_DIGEST_LENGTH];
  SHA256_Final(sha256, &sha256_ctx);

  const uint8_t* signature = eocd + eocd_size - signature_start;
  size_t signature_size = signature_start - FOOTER_SIZE;

  LOG(INFO) << "signature (offset: " << std::hex << (length - signature_start)
            << ", length: " << signature_size << "): " << print_hex(signature, signature_size);

  std::vector<uint8_t> sig_der;
  if (!read_pkcs7(signature, signature_size, &sig_der)) {
    LOG(ERROR) << "Could not find signature DER block";
    return VERIFY_FAILURE;
  }

  // Check to make sure at least one of the keys matches the signature. Since any key can match,
  // we need to try each before determining a verification failure has happened.
  size_t i = 0;
  for (const auto& key : keys) {
    const uint8_t* hash;
    int hash_nid;
    switch (key.hash_len) {
      case SHA_DIGEST_LENGTH:
        hash = sha1;
        hash_nid = NID_sha1;
        break;
      case SHA256_DIGEST_LENGTH:
        hash = sha256;
        hash_nid = NID_sha256;
        break;
      default:
        continue;
    }

    // The 6 bytes is the "(signature_start) $0xff $0xff (comment_size)" that the signing tool appends
    // after the signature itself.
    if (key.key_type == Certificate::KEY_TYPE_RSA) {
      if (!RSA_verify(hash_nid, hash, key.hash_len, sig_der.data(), sig_der.size(),
                      key.rsa.get())) {
        LOG(INFO) << "failed to verify against RSA key " << i;
        continue;
      }

      LOG(INFO) << "whole-file signature verified against RSA key " << i;
      return VERIFY_SUCCESS;
    } else if (key.key_type == Certificate::KEY_TYPE_EC && key.hash_len == SHA256_DIGEST_LENGTH) {
      if (!ECDSA_verify(0, hash, key.hash_len, sig_der.data(), sig_der.size(), key.ec.get())) {
        LOG(INFO) << "failed to verify against EC key " << i;
        continue;
      }

      LOG(INFO) << "whole-file signature verified against EC key " << i;
      return VERIFY_SUCCESS;
    } else {
      LOG(INFO) << "Unknown key type " << key.key_type;
    }
    i++;
  }
//省略代码。。。。。。
}

5、update-binary 执行流程,核心是处理update_script

在上面install.cpp中的InstallPackage中的SetUpNonAbUpdateCommands函数里面会从升级包中读取updater进程。

进入bootable/recovery/updater/update_main.cpp:

int main(int argc, char** argv) {
  // Various things log information to stdout or stderr more or less
  // at random (though we've tried to standardize on stdout).  The
  // log file makes more sense if buffering is turned off so things
  // appear in the right order.
  setbuf(stdout, nullptr);
  setbuf(stderr, nullptr);

  // We don't have logcat yet under recovery. Update logs will always be written to stdout
  // (which is redirected to recovery.log).
  android::base::InitLogging(argv, &UpdaterLogger);

  // Run the libcrypto KAT(known answer tests) based self tests.
  if (BORINGSSL_self_test() != 1) {
    LOG(ERROR) << "Failed to run the boringssl self tests";
    return EXIT_FAILURE;
  }

  if (argc != 4 && argc != 5) {
    LOG(ERROR) << "unexpected number of arguments: " << argc;
    return EXIT_FAILURE;
  }

  char* version = argv[1];
  if ((version[0] != '1' && version[0] != '2' && version[0] != '3') || version[1] != '\0') {
    // We support version 1, 2, or 3.
    LOG(ERROR) << "wrong updater binary API; expected 1, 2, or 3; got " << argv[1];
    return EXIT_FAILURE;
  }

  int fd;
  if (!android::base::ParseInt(argv[2], &fd)) {
    LOG(ERROR) << "Failed to parse fd in " << argv[2];
    return EXIT_FAILURE;
  }

  std::string package_name = argv[3];

  bool is_retry = false;
  if (argc == 5) {
    if (strcmp(argv[4], "retry") == 0) {
      is_retry = true;
    } else {
      LOG(ERROR) << "unexpected argument: " << argv[4];
      return EXIT_FAILURE;
    }
  }

  // Configure edify's functions.
  RegisterBuiltins();
  RegisterInstallFunctions();
  RegisterBlockImageFunctions();
  RegisterDynamicPartitionsFunctions();
  RegisterDeviceExtensions();

  auto sehandle = selinux_android_file_context_handle();
  selinux_android_set_sehandle(sehandle);
  //从update.zip包中将updater-script脚本读到一块动态内存中
  Updater updater(std::make_unique<UpdaterRuntime>(sehandle));
  if (!updater.Init(fd, package_name, is_retry)) {
    return EXIT_FAILURE;
  }

  if (!updater.RunUpdate()) {
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}

6、Updater::Init代码如下:

重点关注下Evaluate

bootable/recovery/updater/updater.cpp

bool Updater::Init(int fd, const std::string_view package_filename, bool is_retry) {
  // Set up the pipe for sending commands back to the parent process.
  cmd_pipe_.reset(fdopen(fd, "wb"));
  if (!cmd_pipe_) {
    LOG(ERROR) << "Failed to open the command pipe";
    return false;
  }

  setlinebuf(cmd_pipe_.get());

  if (!mapped_package_.MapFile(std::string(package_filename))) {
    LOG(ERROR) << "failed to map package " << package_filename;
    return false;
  }
  if (int open_err = OpenArchiveFromMemory(mapped_package_.addr, mapped_package_.length,
                                           std::string(package_filename).c_str(), &package_handle_);
      open_err != 0) {
    LOG(ERROR) << "failed to open package " << package_filename << ": "
               << ErrorCodeString(open_err);
    return false;
  }
  if (!ReadEntryToString(package_handle_, SCRIPT_NAME, &updater_script_)) {
    return false;
  }

  is_retry_ = is_retry;

  return true;
}

bool Updater::RunUpdate() {
  CHECK(runtime_);

  // Parse the script.
  std::unique_ptr<Expr> root;
  int error_count = 0;
  //调用库函数解析脚本
  int error = ParseString(updater_script_, &root, &error_count);
  if (error != 0 || error_count > 0) {
    LOG(ERROR) << error_count << " parse errors";
    return false;
  }

  // Evaluate the parsed script.
  State state(updater_script_, this);
  state.is_retry = is_retry_;

  bool status = Evaluate(&state, root, &result_);
  if (status) {
    fprintf(cmd_pipe_.get(), "ui_print script succeeded: result was [%s]\n", result_.c_str());
    // Even though the script doesn't abort, still log the cause code if result is empty.
    if (result_.empty() && state.cause_code != kNoCause) {
      fprintf(cmd_pipe_.get(), "log cause: %d\n", state.cause_code);
    }
    for (const auto& func : skipped_functions_) {
      LOG(WARNING) << "Skipped executing function " << func;
    }
    return true;
  }

  ParseAndReportErrorCode(&state);
  return false;
}

总结

recovery大致流程描述:(摘自recovery.cpp)

  • OTA INSTALL

  1. main system downloads OTA package to /cache/some-filename.zip

  2. main system writes "--update_package=/cache/some-filename.zip"

  3. main system reboots into recovery

  4. get_args() writes BCB with "boot-recovery" and "--update_package=..."

  5. After this, rebooting will attempt to reinstall the update

  6. InstallPackage() attempts to install the update

  7. NOTE: the package installed must itself be restartable from any point

  8. FinishRecovery() erases BCB

  9. After this, rebooting will (try to) restart the main system

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值