1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.os;
18 
19 import static java.nio.charset.StandardCharsets.UTF_8;
20 
21 import android.annotation.RequiresPermission;
22 import android.annotation.SuppressLint;
23 import android.annotation.SystemApi;
24 import android.annotation.SystemService;
25 import android.app.PendingIntent;
26 import android.content.BroadcastReceiver;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.PackageManager;
32 import android.provider.Settings;
33 import android.telephony.euicc.EuiccManager;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.Display;
37 import android.view.WindowManager;
38 
39 import com.android.internal.logging.MetricsLogger;
40 
41 import libcore.io.Streams;
42 
43 import java.io.BufferedReader;
44 import java.io.ByteArrayInputStream;
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.FileNotFoundException;
48 import java.io.FileReader;
49 import java.io.FileWriter;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.io.RandomAccessFile;
53 import java.security.GeneralSecurityException;
54 import java.security.PublicKey;
55 import java.security.SignatureException;
56 import java.security.cert.CertificateFactory;
57 import java.security.cert.X509Certificate;
58 import java.util.ArrayList;
59 import java.util.Enumeration;
60 import java.util.HashSet;
61 import java.util.Locale;
62 import java.util.concurrent.CountDownLatch;
63 import java.util.concurrent.TimeUnit;
64 import java.util.concurrent.atomic.AtomicBoolean;
65 import java.util.zip.ZipEntry;
66 import java.util.zip.ZipFile;
67 import java.util.zip.ZipInputStream;
68 
69 import sun.security.pkcs.PKCS7;
70 import sun.security.pkcs.SignerInfo;
71 
72 /**
73  * RecoverySystem contains methods for interacting with the Android
74  * recovery system (the separate partition that can be used to install
75  * system updates, wipe user data, etc.)
76  */
77 @SystemService(Context.RECOVERY_SERVICE)
78 public class RecoverySystem {
79     private static final String TAG = "RecoverySystem";
80 
81     /**
82      * Default location of zip file containing public keys (X509
83      * certs) authorized to sign OTA updates.
84      */
85     private static final File DEFAULT_KEYSTORE =
86         new File("/system/etc/security/otacerts.zip");
87 
88     /** Send progress to listeners no more often than this (in ms). */
89     private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
90 
91     private static final long DEFAULT_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 30000L; // 30 s
92 
93     private static final long MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 5000L; // 5 s
94 
95     private static final long MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS = 60000L; // 60 s
96 
97     /** Used to communicate with recovery.  See bootable/recovery/recovery.cpp. */
98     private static final File RECOVERY_DIR = new File("/cache/recovery");
99     private static final File LOG_FILE = new File(RECOVERY_DIR, "log");
100     private static final File LAST_INSTALL_FILE = new File(RECOVERY_DIR, "last_install");
101     private static final String LAST_PREFIX = "last_";
102     private static final String ACTION_EUICC_FACTORY_RESET =
103             "com.android.internal.action.EUICC_FACTORY_RESET";
104 
105     /** used in {@link #wipeEuiccData} as package name of callback intent */
106     private static final String PACKAGE_NAME_WIPING_EUICC_DATA_CALLBACK = "android";
107 
108     /**
109      * The recovery image uses this file to identify the location (i.e. blocks)
110      * of an OTA package on the /data partition. The block map file is
111      * generated by uncrypt.
112      *
113      * @hide
114      */
115     public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map");
116 
117     /**
118      * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be
119      * read by uncrypt.
120      *
121      * @hide
122      */
123     public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file");
124 
125     /**
126      * UNCRYPT_STATUS_FILE stores the time cost (and error code in the case of a failure)
127      * of uncrypt.
128      *
129      * @hide
130      */
131     public static final File UNCRYPT_STATUS_FILE = new File(RECOVERY_DIR, "uncrypt_status");
132 
133     // Length limits for reading files.
134     private static final int LOG_FILE_MAX_LENGTH = 64 * 1024;
135 
136     // Prevent concurrent execution of requests.
137     private static final Object sRequestLock = new Object();
138 
139     private final IRecoverySystem mService;
140 
141     /**
142      * Interface definition for a callback to be invoked regularly as
143      * verification proceeds.
144      */
145     public interface ProgressListener {
146         /**
147          * Called periodically as the verification progresses.
148          *
149          * @param progress  the approximate percentage of the
150          *        verification that has been completed, ranging from 0
151          *        to 100 (inclusive).
152          */
onProgress(int progress)153         public void onProgress(int progress);
154     }
155 
156     /** @return the set of certs that can be used to sign an OTA package. */
getTrustedCerts(File keystore)157     private static HashSet<X509Certificate> getTrustedCerts(File keystore)
158         throws IOException, GeneralSecurityException {
159         HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
160         if (keystore == null) {
161             keystore = DEFAULT_KEYSTORE;
162         }
163         ZipFile zip = new ZipFile(keystore);
164         try {
165             CertificateFactory cf = CertificateFactory.getInstance("X.509");
166             Enumeration<? extends ZipEntry> entries = zip.entries();
167             while (entries.hasMoreElements()) {
168                 ZipEntry entry = entries.nextElement();
169                 InputStream is = zip.getInputStream(entry);
170                 try {
171                     trusted.add((X509Certificate) cf.generateCertificate(is));
172                 } finally {
173                     is.close();
174                 }
175             }
176         } finally {
177             zip.close();
178         }
179         return trusted;
180     }
181 
182     /**
183      * Verify the cryptographic signature of a system update package
184      * before installing it.  Note that the package is also verified
185      * separately by the installer once the device is rebooted into
186      * the recovery system.  This function will return only if the
187      * package was successfully verified; otherwise it will throw an
188      * exception.
189      *
190      * Verification of a package can take significant time, so this
191      * function should not be called from a UI thread.  Interrupting
192      * the thread while this function is in progress will result in a
193      * SecurityException being thrown (and the thread's interrupt flag
194      * will be cleared).
195      *
196      * @param packageFile  the package to be verified
197      * @param listener     an object to receive periodic progress
198      * updates as verification proceeds.  May be null.
199      * @param deviceCertsZipFile  the zip file of certificates whose
200      * public keys we will accept.  Verification succeeds if the
201      * package is signed by the private key corresponding to any
202      * public key in this file.  May be null to use the system default
203      * file (currently "/system/etc/security/otacerts.zip").
204      *
205      * @throws IOException if there were any errors reading the
206      * package or certs files.
207      * @throws GeneralSecurityException if verification failed
208      */
verifyPackage(File packageFile, ProgressListener listener, File deviceCertsZipFile)209     public static void verifyPackage(File packageFile,
210                                      ProgressListener listener,
211                                      File deviceCertsZipFile)
212         throws IOException, GeneralSecurityException {
213         final long fileLen = packageFile.length();
214 
215         final RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
216         try {
217             final long startTimeMillis = System.currentTimeMillis();
218             if (listener != null) {
219                 listener.onProgress(0);
220             }
221 
222             raf.seek(fileLen - 6);
223             byte[] footer = new byte[6];
224             raf.readFully(footer);
225 
226             if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
227                 throw new SignatureException("no signature in file (no footer)");
228             }
229 
230             final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
231             final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
232 
233             byte[] eocd = new byte[commentSize + 22];
234             raf.seek(fileLen - (commentSize + 22));
235             raf.readFully(eocd);
236 
237             // Check that we have found the start of the
238             // end-of-central-directory record.
239             if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
240                 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
241                 throw new SignatureException("no signature in file (bad footer)");
242             }
243 
244             for (int i = 4; i < eocd.length-3; ++i) {
245                 if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
246                     eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
247                     throw new SignatureException("EOCD marker found after start of EOCD");
248                 }
249             }
250 
251             // Parse the signature
252             PKCS7 block =
253                 new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
254 
255             // Take the first certificate from the signature (packages
256             // should contain only one).
257             X509Certificate[] certificates = block.getCertificates();
258             if (certificates == null || certificates.length == 0) {
259                 throw new SignatureException("signature contains no certificates");
260             }
261             X509Certificate cert = certificates[0];
262             PublicKey signatureKey = cert.getPublicKey();
263 
264             SignerInfo[] signerInfos = block.getSignerInfos();
265             if (signerInfos == null || signerInfos.length == 0) {
266                 throw new SignatureException("signature contains no signedData");
267             }
268             SignerInfo signerInfo = signerInfos[0];
269 
270             // Check that the public key of the certificate contained
271             // in the package equals one of our trusted public keys.
272             boolean verified = false;
273             HashSet<X509Certificate> trusted = getTrustedCerts(
274                 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
275             for (X509Certificate c : trusted) {
276                 if (c.getPublicKey().equals(signatureKey)) {
277                     verified = true;
278                     break;
279                 }
280             }
281             if (!verified) {
282                 throw new SignatureException("signature doesn't match any trusted key");
283             }
284 
285             // The signature cert matches a trusted key.  Now verify that
286             // the digest in the cert matches the actual file data.
287             raf.seek(0);
288             final ProgressListener listenerForInner = listener;
289             SignerInfo verifyResult = block.verify(signerInfo, new InputStream() {
290                 // The signature covers all of the OTA package except the
291                 // archive comment and its 2-byte length.
292                 long toRead = fileLen - commentSize - 2;
293                 long soFar = 0;
294 
295                 int lastPercent = 0;
296                 long lastPublishTime = startTimeMillis;
297 
298                 @Override
299                 public int read() throws IOException {
300                     throw new UnsupportedOperationException();
301                 }
302 
303                 @Override
304                 public int read(byte[] b, int off, int len) throws IOException {
305                     if (soFar >= toRead) {
306                         return -1;
307                     }
308                     if (Thread.currentThread().isInterrupted()) {
309                         return -1;
310                     }
311 
312                     int size = len;
313                     if (soFar + size > toRead) {
314                         size = (int)(toRead - soFar);
315                     }
316                     int read = raf.read(b, off, size);
317                     soFar += read;
318 
319                     if (listenerForInner != null) {
320                         long now = System.currentTimeMillis();
321                         int p = (int)(soFar * 100 / toRead);
322                         if (p > lastPercent &&
323                             now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
324                             lastPercent = p;
325                             lastPublishTime = now;
326                             listenerForInner.onProgress(lastPercent);
327                         }
328                     }
329 
330                     return read;
331                 }
332             });
333 
334             final boolean interrupted = Thread.interrupted();
335             if (listener != null) {
336                 listener.onProgress(100);
337             }
338 
339             if (interrupted) {
340                 throw new SignatureException("verification was interrupted");
341             }
342 
343             if (verifyResult == null) {
344                 throw new SignatureException("signature digest verification failed");
345             }
346         } finally {
347             raf.close();
348         }
349 
350         // Additionally verify the package compatibility.
351         if (!readAndVerifyPackageCompatibilityEntry(packageFile)) {
352             throw new SignatureException("package compatibility verification failed");
353         }
354     }
355 
356     /**
357      * Verifies the compatibility entry from an {@link InputStream}.
358      *
359      * @return the verification result.
360      */
verifyPackageCompatibility(InputStream inputStream)361     private static boolean verifyPackageCompatibility(InputStream inputStream) throws IOException {
362         ArrayList<String> list = new ArrayList<>();
363         ZipInputStream zis = new ZipInputStream(inputStream);
364         ZipEntry entry;
365         while ((entry = zis.getNextEntry()) != null) {
366             long entrySize = entry.getSize();
367             if (entrySize > Integer.MAX_VALUE || entrySize < 0) {
368                 throw new IOException(
369                         "invalid entry size (" + entrySize + ") in the compatibility file");
370             }
371             byte[] bytes = new byte[(int) entrySize];
372             Streams.readFully(zis, bytes);
373             list.add(new String(bytes, UTF_8));
374         }
375         if (list.isEmpty()) {
376             throw new IOException("no entries found in the compatibility file");
377         }
378         return (VintfObject.verify(list.toArray(new String[list.size()])) == 0);
379     }
380 
381     /**
382      * Reads and verifies the compatibility entry in an OTA zip package. The compatibility entry is
383      * a zip file (inside the OTA package zip).
384      *
385      * @return {@code true} if the entry doesn't exist or verification passes.
386      */
readAndVerifyPackageCompatibilityEntry(File packageFile)387     private static boolean readAndVerifyPackageCompatibilityEntry(File packageFile)
388             throws IOException {
389         try (ZipFile zip = new ZipFile(packageFile)) {
390             ZipEntry entry = zip.getEntry("compatibility.zip");
391             if (entry == null) {
392                 return true;
393             }
394             InputStream inputStream = zip.getInputStream(entry);
395             return verifyPackageCompatibility(inputStream);
396         }
397     }
398 
399     /**
400      * Verifies the package compatibility info against the current system.
401      *
402      * @param compatibilityFile the {@link File} that contains the package compatibility info.
403      * @throws IOException if there were any errors reading the compatibility file.
404      * @return the compatibility verification result.
405      *
406      * {@hide}
407      */
408     @SystemApi
409     @SuppressLint("Doclava125")
verifyPackageCompatibility(File compatibilityFile)410     public static boolean verifyPackageCompatibility(File compatibilityFile) throws IOException {
411         try (InputStream inputStream = new FileInputStream(compatibilityFile)) {
412             return verifyPackageCompatibility(inputStream);
413         }
414     }
415 
416     /**
417      * Process a given package with uncrypt. No-op if the package is not on the
418      * /data partition.
419      *
420      * @param Context      the Context to use
421      * @param packageFile  the package to be processed
422      * @param listener     an object to receive periodic progress updates as
423      *                     processing proceeds.  May be null.
424      * @param handler      the Handler upon which the callbacks will be
425      *                     executed.
426      *
427      * @throws IOException if there were any errors processing the package file.
428      *
429      * @hide
430      */
431     @SystemApi
432     @RequiresPermission(android.Manifest.permission.RECOVERY)
processPackage(Context context, File packageFile, final ProgressListener listener, final Handler handler)433     public static void processPackage(Context context,
434                                       File packageFile,
435                                       final ProgressListener listener,
436                                       final Handler handler)
437             throws IOException {
438         String filename = packageFile.getCanonicalPath();
439         if (!filename.startsWith("/data/")) {
440             return;
441         }
442 
443         RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
444         IRecoverySystemProgressListener progressListener = null;
445         if (listener != null) {
446             final Handler progressHandler;
447             if (handler != null) {
448                 progressHandler = handler;
449             } else {
450                 progressHandler = new Handler(context.getMainLooper());
451             }
452             progressListener = new IRecoverySystemProgressListener.Stub() {
453                 int lastProgress = 0;
454                 long lastPublishTime = System.currentTimeMillis();
455 
456                 @Override
457                 public void onProgress(final int progress) {
458                     final long now = System.currentTimeMillis();
459                     progressHandler.post(new Runnable() {
460                         @Override
461                         public void run() {
462                             if (progress > lastProgress &&
463                                     now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
464                                 lastProgress = progress;
465                                 lastPublishTime = now;
466                                 listener.onProgress(progress);
467                             }
468                         }
469                     });
470                 }
471             };
472         }
473 
474         if (!rs.uncrypt(filename, progressListener)) {
475             throw new IOException("process package failed");
476         }
477     }
478 
479     /**
480      * Process a given package with uncrypt. No-op if the package is not on the
481      * /data partition.
482      *
483      * @param Context      the Context to use
484      * @param packageFile  the package to be processed
485      * @param listener     an object to receive periodic progress updates as
486      *                     processing proceeds.  May be null.
487      *
488      * @throws IOException if there were any errors processing the package file.
489      *
490      * @hide
491      */
492     @SystemApi
493     @RequiresPermission(android.Manifest.permission.RECOVERY)
processPackage(Context context, File packageFile, final ProgressListener listener)494     public static void processPackage(Context context,
495                                       File packageFile,
496                                       final ProgressListener listener)
497             throws IOException {
498         processPackage(context, packageFile, listener, null);
499     }
500 
501     /**
502      * Reboots the device in order to install the given update
503      * package.
504      * Requires the {@link android.Manifest.permission#REBOOT} permission.
505      *
506      * @param context      the Context to use
507      * @param packageFile  the update package to install.  Must be on
508      * a partition mountable by recovery.  (The set of partitions
509      * known to recovery may vary from device to device.  Generally,
510      * /cache and /data are safe.)
511      *
512      * @throws IOException  if writing the recovery command file
513      * fails, or if the reboot itself fails.
514      */
515     @RequiresPermission(android.Manifest.permission.RECOVERY)
installPackage(Context context, File packageFile)516     public static void installPackage(Context context, File packageFile)
517             throws IOException {
518         installPackage(context, packageFile, false);
519     }
520 
521     /**
522      * If the package hasn't been processed (i.e. uncrypt'd), set up
523      * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the
524      * reboot.
525      *
526      * @param context      the Context to use
527      * @param packageFile  the update package to install.  Must be on a
528      * partition mountable by recovery.
529      * @param processed    if the package has been processed (uncrypt'd).
530      *
531      * @throws IOException if writing the recovery command file fails, or if
532      * the reboot itself fails.
533      *
534      * @hide
535      */
536     @SystemApi
537     @RequiresPermission(android.Manifest.permission.RECOVERY)
installPackage(Context context, File packageFile, boolean processed)538     public static void installPackage(Context context, File packageFile, boolean processed)
539             throws IOException {
540         synchronized (sRequestLock) {
541             LOG_FILE.delete();
542             // Must delete the file in case it was created by system server.
543             UNCRYPT_PACKAGE_FILE.delete();
544 
545             String filename = packageFile.getCanonicalPath();
546             Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
547 
548             // If the package name ends with "_s.zip", it's a security update.
549             boolean securityUpdate = filename.endsWith("_s.zip");
550 
551             // If the package is on the /data partition, the package needs to
552             // be processed (i.e. uncrypt'd). The caller specifies if that has
553             // been done in 'processed' parameter.
554             if (filename.startsWith("/data/")) {
555                 if (processed) {
556                     if (!BLOCK_MAP_FILE.exists()) {
557                         Log.e(TAG, "Package claimed to have been processed but failed to find "
558                                 + "the block map file.");
559                         throw new IOException("Failed to find block map file");
560                     }
561                 } else {
562                     FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
563                     try {
564                         uncryptFile.write(filename + "\n");
565                     } finally {
566                         uncryptFile.close();
567                     }
568                     // UNCRYPT_PACKAGE_FILE needs to be readable and writable
569                     // by system server.
570                     if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
571                             || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
572                         Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
573                     }
574 
575                     BLOCK_MAP_FILE.delete();
576                 }
577 
578                 // If the package is on the /data partition, use the block map
579                 // file as the package name instead.
580                 filename = "@/cache/recovery/block.map";
581             }
582 
583             final String filenameArg = "--update_package=" + filename + "\n";
584             final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
585             final String securityArg = "--security\n";
586 
587             String command = filenameArg + localeArg;
588             if (securityUpdate) {
589                 command += securityArg;
590             }
591 
592             RecoverySystem rs = (RecoverySystem) context.getSystemService(
593                     Context.RECOVERY_SERVICE);
594             if (!rs.setupBcb(command)) {
595                 throw new IOException("Setup BCB failed");
596             }
597 
598             // Having set up the BCB (bootloader control block), go ahead and reboot
599             PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
600             String reason = PowerManager.REBOOT_RECOVERY_UPDATE;
601 
602             // On TV, reboot quiescently if the screen is off
603             if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
604                 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
605                 if (wm.getDefaultDisplay().getState() != Display.STATE_ON) {
606                     reason += ",quiescent";
607                 }
608             }
609             pm.reboot(reason);
610 
611             throw new IOException("Reboot failed (no permissions?)");
612         }
613     }
614 
615     /**
616      * Schedule to install the given package on next boot. The caller needs to
617      * ensure that the package must have been processed (uncrypt'd) if needed.
618      * It sets up the command in BCB (bootloader control block), which will
619      * be read by the bootloader and the recovery image.
620      *
621      * @param Context      the Context to use.
622      * @param packageFile  the package to be installed.
623      *
624      * @throws IOException if there were any errors setting up the BCB.
625      *
626      * @hide
627      */
628     @SystemApi
629     @RequiresPermission(android.Manifest.permission.RECOVERY)
scheduleUpdateOnBoot(Context context, File packageFile)630     public static void scheduleUpdateOnBoot(Context context, File packageFile)
631             throws IOException {
632         String filename = packageFile.getCanonicalPath();
633         boolean securityUpdate = filename.endsWith("_s.zip");
634 
635         // If the package is on the /data partition, use the block map file as
636         // the package name instead.
637         if (filename.startsWith("/data/")) {
638             filename = "@/cache/recovery/block.map";
639         }
640 
641         final String filenameArg = "--update_package=" + filename + "\n";
642         final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
643         final String securityArg = "--security\n";
644 
645         String command = filenameArg + localeArg;
646         if (securityUpdate) {
647             command += securityArg;
648         }
649 
650         RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
651         if (!rs.setupBcb(command)) {
652             throw new IOException("schedule update on boot failed");
653         }
654     }
655 
656     /**
657      * Cancel any scheduled update by clearing up the BCB (bootloader control
658      * block).
659      *
660      * @param Context      the Context to use.
661      *
662      * @throws IOException if there were any errors clearing up the BCB.
663      *
664      * @hide
665      */
666     @SystemApi
667     @RequiresPermission(android.Manifest.permission.RECOVERY)
cancelScheduledUpdate(Context context)668     public static void cancelScheduledUpdate(Context context)
669             throws IOException {
670         RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
671         if (!rs.clearBcb()) {
672             throw new IOException("cancel scheduled update failed");
673         }
674     }
675 
676     /**
677      * Reboots the device and wipes the user data and cache
678      * partitions.  This is sometimes called a "factory reset", which
679      * is something of a misnomer because the system partition is not
680      * restored to its factory state.  Requires the
681      * {@link android.Manifest.permission#REBOOT} permission.
682      *
683      * @param context  the Context to use
684      *
685      * @throws IOException  if writing the recovery command file
686      * fails, or if the reboot itself fails.
687      * @throws SecurityException if the current user is not allowed to wipe data.
688      */
rebootWipeUserData(Context context)689     public static void rebootWipeUserData(Context context) throws IOException {
690         rebootWipeUserData(context, false /* shutdown */, context.getPackageName(),
691                 false /* force */, false /* wipeEuicc */);
692     }
693 
694     /** {@hide} */
rebootWipeUserData(Context context, String reason)695     public static void rebootWipeUserData(Context context, String reason) throws IOException {
696         rebootWipeUserData(context, false /* shutdown */, reason, false /* force */,
697                 false /* wipeEuicc */);
698     }
699 
700     /** {@hide} */
rebootWipeUserData(Context context, boolean shutdown)701     public static void rebootWipeUserData(Context context, boolean shutdown)
702             throws IOException {
703         rebootWipeUserData(context, shutdown, context.getPackageName(), false /* force */,
704                 false /* wipeEuicc */);
705     }
706 
707     /** {@hide} */
rebootWipeUserData(Context context, boolean shutdown, String reason, boolean force)708     public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
709             boolean force) throws IOException {
710         rebootWipeUserData(context, shutdown, reason, force, false /* wipeEuicc */);
711     }
712 
713     /**
714      * Reboots the device and wipes the user data and cache
715      * partitions.  This is sometimes called a "factory reset", which
716      * is something of a misnomer because the system partition is not
717      * restored to its factory state.  Requires the
718      * {@link android.Manifest.permission#REBOOT} permission.
719      *
720      * @param context   the Context to use
721      * @param shutdown  if true, the device will be powered down after
722      *                  the wipe completes, rather than being rebooted
723      *                  back to the regular system.
724      * @param reason    the reason for the wipe that is visible in the logs
725      * @param force     whether the {@link UserManager.DISALLOW_FACTORY_RESET} user restriction
726      *                  should be ignored
727      * @param wipeEuicc whether wipe the euicc data
728      *
729      * @throws IOException  if writing the recovery command file
730      * fails, or if the reboot itself fails.
731      * @throws SecurityException if the current user is not allowed to wipe data.
732      *
733      * @hide
734      */
rebootWipeUserData(Context context, boolean shutdown, String reason, boolean force, boolean wipeEuicc)735     public static void rebootWipeUserData(Context context, boolean shutdown, String reason,
736             boolean force, boolean wipeEuicc) throws IOException {
737         UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
738         if (!force && um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
739             throw new SecurityException("Wiping data is not allowed for this user.");
740         }
741         final ConditionVariable condition = new ConditionVariable();
742 
743         Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
744         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
745                 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
746         context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM,
747                 android.Manifest.permission.MASTER_CLEAR,
748                 new BroadcastReceiver() {
749                     @Override
750                     public void onReceive(Context context, Intent intent) {
751                         condition.open();
752                     }
753                 }, null, 0, null, null);
754 
755         // Block until the ordered broadcast has completed.
756         condition.block();
757 
758         if (wipeEuicc) {
759             wipeEuiccData(context, PACKAGE_NAME_WIPING_EUICC_DATA_CALLBACK);
760         }
761 
762         String shutdownArg = null;
763         if (shutdown) {
764             shutdownArg = "--shutdown_after";
765         }
766 
767         String reasonArg = null;
768         if (!TextUtils.isEmpty(reason)) {
769             reasonArg = "--reason=" + sanitizeArg(reason);
770         }
771 
772         final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
773         bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
774     }
775 
776     /**
777      * Returns whether wipe Euicc data successfully or not.
778      *
779      * @param packageName the package name of the caller app.
780      *
781      * @hide
782      */
wipeEuiccData(Context context, final String packageName)783     public static boolean wipeEuiccData(Context context, final String packageName) {
784         ContentResolver cr = context.getContentResolver();
785         if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) {
786             // If the eUICC isn't provisioned, there's no reason to either wipe or retain profiles,
787             // as there's nothing to wipe nor retain.
788             Log.d(TAG, "Skipping eUICC wipe/retain as it is not provisioned");
789             return true;
790         }
791 
792         EuiccManager euiccManager = (EuiccManager) context.getSystemService(
793                 Context.EUICC_SERVICE);
794         if (euiccManager != null && euiccManager.isEnabled()) {
795             CountDownLatch euiccFactoryResetLatch = new CountDownLatch(1);
796             final AtomicBoolean wipingSucceeded = new AtomicBoolean(false);
797 
798             BroadcastReceiver euiccWipeFinishReceiver = new BroadcastReceiver() {
799                 @Override
800                 public void onReceive(Context context, Intent intent) {
801                     if (ACTION_EUICC_FACTORY_RESET.equals(intent.getAction())) {
802                         if (getResultCode() != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
803                             int detailedCode = intent.getIntExtra(
804                                     EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0);
805                             Log.e(TAG, "Error wiping euicc data, Detailed code = "
806                                     + detailedCode);
807                         } else {
808                             Log.d(TAG, "Successfully wiped euicc data.");
809                             wipingSucceeded.set(true /* newValue */);
810                         }
811                         euiccFactoryResetLatch.countDown();
812                     }
813                 }
814             };
815 
816             Intent intent = new Intent(ACTION_EUICC_FACTORY_RESET);
817             intent.setPackage(packageName);
818             PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
819                     context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
820             IntentFilter filterConsent = new IntentFilter();
821             filterConsent.addAction(ACTION_EUICC_FACTORY_RESET);
822             HandlerThread euiccHandlerThread = new HandlerThread("euiccWipeFinishReceiverThread");
823             euiccHandlerThread.start();
824             Handler euiccHandler = new Handler(euiccHandlerThread.getLooper());
825             context.getApplicationContext()
826                     .registerReceiver(euiccWipeFinishReceiver, filterConsent, null, euiccHandler);
827             euiccManager.eraseSubscriptions(callbackIntent);
828             try {
829                 long waitingTimeMillis = Settings.Global.getLong(
830                         context.getContentResolver(),
831                         Settings.Global.EUICC_FACTORY_RESET_TIMEOUT_MILLIS,
832                         DEFAULT_EUICC_FACTORY_RESET_TIMEOUT_MILLIS);
833                 if (waitingTimeMillis < MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS) {
834                     waitingTimeMillis = MIN_EUICC_FACTORY_RESET_TIMEOUT_MILLIS;
835                 } else if (waitingTimeMillis > MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS) {
836                     waitingTimeMillis = MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS;
837                 }
838                 if (!euiccFactoryResetLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) {
839                     Log.e(TAG, "Timeout wiping eUICC data.");
840                     return false;
841                 }
842             } catch (InterruptedException e) {
843                 Thread.currentThread().interrupt();
844                 Log.e(TAG, "Wiping eUICC data interrupted", e);
845                 return false;
846             } finally {
847                 context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver);
848             }
849             return wipingSucceeded.get();
850         }
851         return false;
852     }
853 
854     /** {@hide} */
rebootPromptAndWipeUserData(Context context, String reason)855     public static void rebootPromptAndWipeUserData(Context context, String reason)
856             throws IOException {
857         String reasonArg = null;
858         if (!TextUtils.isEmpty(reason)) {
859             reasonArg = "--reason=" + sanitizeArg(reason);
860         }
861 
862         final String localeArg = "--locale=" + Locale.getDefault().toString();
863         bootCommand(context, null, "--prompt_and_wipe_data", reasonArg, localeArg);
864     }
865 
866     /**
867      * Reboot into the recovery system to wipe the /cache partition.
868      * @throws IOException if something goes wrong.
869      */
rebootWipeCache(Context context)870     public static void rebootWipeCache(Context context) throws IOException {
871         rebootWipeCache(context, context.getPackageName());
872     }
873 
874     /** {@hide} */
rebootWipeCache(Context context, String reason)875     public static void rebootWipeCache(Context context, String reason) throws IOException {
876         String reasonArg = null;
877         if (!TextUtils.isEmpty(reason)) {
878             reasonArg = "--reason=" + sanitizeArg(reason);
879         }
880 
881         final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
882         bootCommand(context, "--wipe_cache", reasonArg, localeArg);
883     }
884 
885     /**
886      * Reboot into recovery and wipe the A/B device.
887      *
888      * @param Context      the Context to use.
889      * @param packageFile  the wipe package to be applied.
890      * @param reason       the reason to wipe.
891      *
892      * @throws IOException if something goes wrong.
893      *
894      * @hide
895      */
896     @SystemApi
897     @RequiresPermission(allOf = {
898             android.Manifest.permission.RECOVERY,
899             android.Manifest.permission.REBOOT
900     })
rebootWipeAb(Context context, File packageFile, String reason)901     public static void rebootWipeAb(Context context, File packageFile, String reason)
902             throws IOException {
903         String reasonArg = null;
904         if (!TextUtils.isEmpty(reason)) {
905             reasonArg = "--reason=" + sanitizeArg(reason);
906         }
907 
908         final String filename = packageFile.getCanonicalPath();
909         final String filenameArg = "--wipe_package=" + filename;
910         final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ;
911         bootCommand(context, "--wipe_ab", filenameArg, reasonArg, localeArg);
912     }
913 
914     /**
915      * Reboot into the recovery system with the supplied argument.
916      * @param args to pass to the recovery utility.
917      * @throws IOException if something goes wrong.
918      */
bootCommand(Context context, String... args)919     private static void bootCommand(Context context, String... args) throws IOException {
920         LOG_FILE.delete();
921 
922         StringBuilder command = new StringBuilder();
923         for (String arg : args) {
924             if (!TextUtils.isEmpty(arg)) {
925                 command.append(arg);
926                 command.append("\n");
927             }
928         }
929 
930         // Write the command into BCB (bootloader control block) and boot from
931         // there. Will not return unless failed.
932         RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
933         rs.rebootRecoveryWithCommand(command.toString());
934 
935         throw new IOException("Reboot failed (no permissions?)");
936     }
937 
938     // Read last_install; then report time (in seconds) and I/O (in MiB) for
939     // this update to tron.
940     // Only report on the reboots immediately after an OTA update.
parseLastInstallLog(Context context)941     private static void parseLastInstallLog(Context context) {
942         try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) {
943             String line = null;
944             int bytesWrittenInMiB = -1, bytesStashedInMiB = -1;
945             int timeTotal = -1;
946             int uncryptTime = -1;
947             int sourceVersion = -1;
948             int temperatureStart = -1;
949             int temperatureEnd = -1;
950             int temperatureMax = -1;
951             int errorCode = -1;
952             int causeCode = -1;
953 
954             while ((line = in.readLine()) != null) {
955                 // Here is an example of lines in last_install:
956                 // ...
957                 // time_total: 101
958                 // bytes_written_vendor: 51074
959                 // bytes_stashed_vendor: 200
960                 int numIndex = line.indexOf(':');
961                 if (numIndex == -1 || numIndex + 1 >= line.length()) {
962                     continue;
963                 }
964                 String numString = line.substring(numIndex + 1).trim();
965                 long parsedNum;
966                 try {
967                     parsedNum = Long.parseLong(numString);
968                 } catch (NumberFormatException ignored) {
969                     Log.e(TAG, "Failed to parse numbers in " + line);
970                     continue;
971                 }
972 
973                 final int MiB = 1024 * 1024;
974                 int scaled;
975                 try {
976                     if (line.startsWith("bytes")) {
977                         scaled = Math.toIntExact(parsedNum / MiB);
978                     } else {
979                         scaled = Math.toIntExact(parsedNum);
980                     }
981                 } catch (ArithmeticException ignored) {
982                     Log.e(TAG, "Number overflows in " + line);
983                     continue;
984                 }
985 
986                 if (line.startsWith("time")) {
987                     timeTotal = scaled;
988                 } else if (line.startsWith("uncrypt_time")) {
989                     uncryptTime = scaled;
990                 } else if (line.startsWith("source_build")) {
991                     sourceVersion = scaled;
992                 } else if (line.startsWith("bytes_written")) {
993                     bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled :
994                             bytesWrittenInMiB + scaled;
995                 } else if (line.startsWith("bytes_stashed")) {
996                     bytesStashedInMiB = (bytesStashedInMiB == -1) ? scaled :
997                             bytesStashedInMiB + scaled;
998                 } else if (line.startsWith("temperature_start")) {
999                     temperatureStart = scaled;
1000                 } else if (line.startsWith("temperature_end")) {
1001                     temperatureEnd = scaled;
1002                 } else if (line.startsWith("temperature_max")) {
1003                     temperatureMax = scaled;
1004                 } else if (line.startsWith("error")) {
1005                     errorCode = scaled;
1006                 } else if (line.startsWith("cause")) {
1007                     causeCode = scaled;
1008                 }
1009             }
1010 
1011             // Don't report data to tron if corresponding entry isn't found in last_install.
1012             if (timeTotal != -1) {
1013                 MetricsLogger.histogram(context, "ota_time_total", timeTotal);
1014             }
1015             if (uncryptTime != -1) {
1016                 MetricsLogger.histogram(context, "ota_uncrypt_time", uncryptTime);
1017             }
1018             if (sourceVersion != -1) {
1019                 MetricsLogger.histogram(context, "ota_source_version", sourceVersion);
1020             }
1021             if (bytesWrittenInMiB != -1) {
1022                 MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB);
1023             }
1024             if (bytesStashedInMiB != -1) {
1025                 MetricsLogger.histogram(context, "ota_stashed_in_MiBs", bytesStashedInMiB);
1026             }
1027             if (temperatureStart != -1) {
1028                 MetricsLogger.histogram(context, "ota_temperature_start", temperatureStart);
1029             }
1030             if (temperatureEnd != -1) {
1031                 MetricsLogger.histogram(context, "ota_temperature_end", temperatureEnd);
1032             }
1033             if (temperatureMax != -1) {
1034                 MetricsLogger.histogram(context, "ota_temperature_max", temperatureMax);
1035             }
1036             if (errorCode != -1) {
1037                 MetricsLogger.histogram(context, "ota_non_ab_error_code", errorCode);
1038             }
1039             if (causeCode != -1) {
1040                 MetricsLogger.histogram(context, "ota_non_ab_cause_code", causeCode);
1041             }
1042 
1043         } catch (IOException e) {
1044             Log.e(TAG, "Failed to read lines in last_install", e);
1045         }
1046     }
1047 
1048     /**
1049      * Called after booting to process and remove recovery-related files.
1050      * @return the log file from recovery, or null if none was found.
1051      *
1052      * @hide
1053      */
handleAftermath(Context context)1054     public static String handleAftermath(Context context) {
1055         // Record the tail of the LOG_FILE
1056         String log = null;
1057         try {
1058             log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
1059         } catch (FileNotFoundException e) {
1060             Log.i(TAG, "No recovery log file");
1061         } catch (IOException e) {
1062             Log.e(TAG, "Error reading recovery log", e);
1063         }
1064 
1065         if (log != null) {
1066             parseLastInstallLog(context);
1067         }
1068 
1069         // Only remove the OTA package if it's partially processed (uncrypt'd).
1070         boolean reservePackage = BLOCK_MAP_FILE.exists();
1071         if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) {
1072             String filename = null;
1073             try {
1074                 filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null);
1075             } catch (IOException e) {
1076                 Log.e(TAG, "Error reading uncrypt file", e);
1077             }
1078 
1079             // Remove the OTA package on /data that has been (possibly
1080             // partially) processed. (Bug: 24973532)
1081             if (filename != null && filename.startsWith("/data")) {
1082                 if (UNCRYPT_PACKAGE_FILE.delete()) {
1083                     Log.i(TAG, "Deleted: " + filename);
1084                 } else {
1085                     Log.e(TAG, "Can't delete: " + filename);
1086                 }
1087             }
1088         }
1089 
1090         // We keep the update logs (beginning with LAST_PREFIX), and optionally
1091         // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE
1092         // will be created at the end of a successful uncrypt. If seeing this
1093         // file, we keep the block map file and the file that contains the
1094         // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for
1095         // GmsCore to avoid re-downloading everything again.
1096         String[] names = RECOVERY_DIR.list();
1097         for (int i = 0; names != null && i < names.length; i++) {
1098             if (names[i].startsWith(LAST_PREFIX)) continue;
1099             if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue;
1100             if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue;
1101 
1102             recursiveDelete(new File(RECOVERY_DIR, names[i]));
1103         }
1104 
1105         return log;
1106     }
1107 
1108     /**
1109      * Internally, delete a given file or directory recursively.
1110      */
recursiveDelete(File name)1111     private static void recursiveDelete(File name) {
1112         if (name.isDirectory()) {
1113             String[] files = name.list();
1114             for (int i = 0; files != null && i < files.length; i++) {
1115                 File f = new File(name, files[i]);
1116                 recursiveDelete(f);
1117             }
1118         }
1119 
1120         if (!name.delete()) {
1121             Log.e(TAG, "Can't delete: " + name);
1122         } else {
1123             Log.i(TAG, "Deleted: " + name);
1124         }
1125     }
1126 
1127     /**
1128      * Talks to RecoverySystemService via Binder to trigger uncrypt.
1129      */
uncrypt(String packageFile, IRecoverySystemProgressListener listener)1130     private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) {
1131         try {
1132             return mService.uncrypt(packageFile, listener);
1133         } catch (RemoteException unused) {
1134         }
1135         return false;
1136     }
1137 
1138     /**
1139      * Talks to RecoverySystemService via Binder to set up the BCB.
1140      */
setupBcb(String command)1141     private boolean setupBcb(String command) {
1142         try {
1143             return mService.setupBcb(command);
1144         } catch (RemoteException unused) {
1145         }
1146         return false;
1147     }
1148 
1149     /**
1150      * Talks to RecoverySystemService via Binder to clear up the BCB.
1151      */
clearBcb()1152     private boolean clearBcb() {
1153         try {
1154             return mService.clearBcb();
1155         } catch (RemoteException unused) {
1156         }
1157         return false;
1158     }
1159 
1160     /**
1161      * Talks to RecoverySystemService via Binder to set up the BCB command and
1162      * reboot into recovery accordingly.
1163      */
rebootRecoveryWithCommand(String command)1164     private void rebootRecoveryWithCommand(String command) {
1165         try {
1166             mService.rebootRecoveryWithCommand(command);
1167         } catch (RemoteException ignored) {
1168         }
1169     }
1170 
1171     /**
1172      * Internally, recovery treats each line of the command file as a separate
1173      * argv, so we only need to protect against newlines and nulls.
1174      */
sanitizeArg(String arg)1175     private static String sanitizeArg(String arg) {
1176         arg = arg.replace('\0', '?');
1177         arg = arg.replace('\n', '?');
1178         return arg;
1179     }
1180 
1181 
1182     /**
1183      * @removed Was previously made visible by accident.
1184      */
RecoverySystem()1185     public RecoverySystem() {
1186         mService = null;
1187     }
1188 
1189     /**
1190      * @hide
1191      */
RecoverySystem(IRecoverySystem service)1192     public RecoverySystem(IRecoverySystem service) {
1193         mService = service;
1194     }
1195 }
1196