提示: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
-
main system downloads OTA package to /cache/some-filename.zip
-
main system writes "--update_package=/cache/some-filename.zip"
-
main system reboots into recovery
-
get_args() writes BCB with "boot-recovery" and "--update_package=..."
-
After this, rebooting will attempt to reinstall the update
-
InstallPackage() attempts to install the update
-
NOTE: the package installed must itself be restartable from any point
-
FinishRecovery() erases BCB
-
After this, rebooting will (try to) restart the main system