Home | History | Annotate | Download | only in os
      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 android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.os.UserManager;
     23 import android.text.TextUtils;
     24 import android.util.Log;
     25 
     26 import java.io.ByteArrayInputStream;
     27 import java.io.File;
     28 import java.io.FileNotFoundException;
     29 import java.io.FileWriter;
     30 import java.io.IOException;
     31 import java.io.InputStream;
     32 import java.io.RandomAccessFile;
     33 import java.security.GeneralSecurityException;
     34 import java.security.PublicKey;
     35 import java.security.Signature;
     36 import java.security.SignatureException;
     37 import java.security.cert.CertificateFactory;
     38 import java.security.cert.X509Certificate;
     39 import java.util.Enumeration;
     40 import java.util.HashSet;
     41 import java.util.Iterator;
     42 import java.util.List;
     43 import java.util.Locale;
     44 import java.util.zip.ZipEntry;
     45 import java.util.zip.ZipFile;
     46 
     47 import org.apache.harmony.security.asn1.BerInputStream;
     48 import org.apache.harmony.security.pkcs7.ContentInfo;
     49 import org.apache.harmony.security.pkcs7.SignedData;
     50 import org.apache.harmony.security.pkcs7.SignerInfo;
     51 import org.apache.harmony.security.x509.Certificate;
     52 
     53 /**
     54  * RecoverySystem contains methods for interacting with the Android
     55  * recovery system (the separate partition that can be used to install
     56  * system updates, wipe user data, etc.)
     57  */
     58 public class RecoverySystem {
     59     private static final String TAG = "RecoverySystem";
     60 
     61     /**
     62      * Default location of zip file containing public keys (X509
     63      * certs) authorized to sign OTA updates.
     64      */
     65     private static final File DEFAULT_KEYSTORE =
     66         new File("/system/etc/security/otacerts.zip");
     67 
     68     /** Send progress to listeners no more often than this (in ms). */
     69     private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
     70 
     71     /** Used to communicate with recovery.  See bootable/recovery/recovery.c. */
     72     private static File RECOVERY_DIR = new File("/cache/recovery");
     73     private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
     74     private static File LOG_FILE = new File(RECOVERY_DIR, "log");
     75     private static String LAST_PREFIX = "last_";
     76 
     77     // Length limits for reading files.
     78     private static int LOG_FILE_MAX_LENGTH = 64 * 1024;
     79 
     80     /**
     81      * Interface definition for a callback to be invoked regularly as
     82      * verification proceeds.
     83      */
     84     public interface ProgressListener {
     85         /**
     86          * Called periodically as the verification progresses.
     87          *
     88          * @param progress  the approximate percentage of the
     89          *        verification that has been completed, ranging from 0
     90          *        to 100 (inclusive).
     91          */
     92         public void onProgress(int progress);
     93     }
     94 
     95     /** @return the set of certs that can be used to sign an OTA package. */
     96     private static HashSet<X509Certificate> getTrustedCerts(File keystore)
     97         throws IOException, GeneralSecurityException {
     98         HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
     99         if (keystore == null) {
    100             keystore = DEFAULT_KEYSTORE;
    101         }
    102         ZipFile zip = new ZipFile(keystore);
    103         try {
    104             CertificateFactory cf = CertificateFactory.getInstance("X.509");
    105             Enumeration<? extends ZipEntry> entries = zip.entries();
    106             while (entries.hasMoreElements()) {
    107                 ZipEntry entry = entries.nextElement();
    108                 InputStream is = zip.getInputStream(entry);
    109                 try {
    110                     trusted.add((X509Certificate) cf.generateCertificate(is));
    111                 } finally {
    112                     is.close();
    113                 }
    114             }
    115         } finally {
    116             zip.close();
    117         }
    118         return trusted;
    119     }
    120 
    121     /**
    122      * Verify the cryptographic signature of a system update package
    123      * before installing it.  Note that the package is also verified
    124      * separately by the installer once the device is rebooted into
    125      * the recovery system.  This function will return only if the
    126      * package was successfully verified; otherwise it will throw an
    127      * exception.
    128      *
    129      * Verification of a package can take significant time, so this
    130      * function should not be called from a UI thread.  Interrupting
    131      * the thread while this function is in progress will result in a
    132      * SecurityException being thrown (and the thread's interrupt flag
    133      * will be cleared).
    134      *
    135      * @param packageFile  the package to be verified
    136      * @param listener     an object to receive periodic progress
    137      * updates as verification proceeds.  May be null.
    138      * @param deviceCertsZipFile  the zip file of certificates whose
    139      * public keys we will accept.  Verification succeeds if the
    140      * package is signed by the private key corresponding to any
    141      * public key in this file.  May be null to use the system default
    142      * file (currently "/system/etc/security/otacerts.zip").
    143      *
    144      * @throws IOException if there were any errors reading the
    145      * package or certs files.
    146      * @throws GeneralSecurityException if verification failed
    147      */
    148     public static void verifyPackage(File packageFile,
    149                                      ProgressListener listener,
    150                                      File deviceCertsZipFile)
    151         throws IOException, GeneralSecurityException {
    152         long fileLen = packageFile.length();
    153 
    154         RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
    155         try {
    156             int lastPercent = 0;
    157             long lastPublishTime = System.currentTimeMillis();
    158             if (listener != null) {
    159                 listener.onProgress(lastPercent);
    160             }
    161 
    162             raf.seek(fileLen - 6);
    163             byte[] footer = new byte[6];
    164             raf.readFully(footer);
    165 
    166             if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
    167                 throw new SignatureException("no signature in file (no footer)");
    168             }
    169 
    170             int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
    171             int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
    172 
    173             byte[] eocd = new byte[commentSize + 22];
    174             raf.seek(fileLen - (commentSize + 22));
    175             raf.readFully(eocd);
    176 
    177             // Check that we have found the start of the
    178             // end-of-central-directory record.
    179             if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
    180                 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
    181                 throw new SignatureException("no signature in file (bad footer)");
    182             }
    183 
    184             for (int i = 4; i < eocd.length-3; ++i) {
    185                 if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
    186                     eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
    187                     throw new SignatureException("EOCD marker found after start of EOCD");
    188                 }
    189             }
    190 
    191             // The following code is largely copied from
    192             // JarUtils.verifySignature().  We could just *call* that
    193             // method here if that function didn't read the entire
    194             // input (ie, the whole OTA package) into memory just to
    195             // compute its message digest.
    196 
    197             BerInputStream bis = new BerInputStream(
    198                 new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
    199             ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
    200             SignedData signedData = info.getSignedData();
    201             if (signedData == null) {
    202                 throw new IOException("signedData is null");
    203             }
    204             List<Certificate> encCerts = signedData.getCertificates();
    205             if (encCerts.isEmpty()) {
    206                 throw new IOException("encCerts is empty");
    207             }
    208             // Take the first certificate from the signature (packages
    209             // should contain only one).
    210             Iterator<Certificate> it = encCerts.iterator();
    211             X509Certificate cert = null;
    212             if (it.hasNext()) {
    213                 CertificateFactory cf = CertificateFactory.getInstance("X.509");
    214                 InputStream is = new ByteArrayInputStream(it.next().getEncoded());
    215                 cert = (X509Certificate) cf.generateCertificate(is);
    216             } else {
    217                 throw new SignatureException("signature contains no certificates");
    218             }
    219 
    220             List<SignerInfo> sigInfos = signedData.getSignerInfos();
    221             SignerInfo sigInfo;
    222             if (!sigInfos.isEmpty()) {
    223                 sigInfo = (SignerInfo)sigInfos.get(0);
    224             } else {
    225                 throw new IOException("no signer infos!");
    226             }
    227 
    228             // Check that the public key of the certificate contained
    229             // in the package equals one of our trusted public keys.
    230 
    231             HashSet<X509Certificate> trusted = getTrustedCerts(
    232                 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
    233 
    234             PublicKey signatureKey = cert.getPublicKey();
    235             boolean verified = false;
    236             for (X509Certificate c : trusted) {
    237                 if (c.getPublicKey().equals(signatureKey)) {
    238                     verified = true;
    239                     break;
    240                 }
    241             }
    242             if (!verified) {
    243                 throw new SignatureException("signature doesn't match any trusted key");
    244             }
    245 
    246             // The signature cert matches a trusted key.  Now verify that
    247             // the digest in the cert matches the actual file data.
    248 
    249             // The verifier in recovery only handles SHA1withRSA and
    250             // SHA256withRSA signatures.  SignApk chooses which to use
    251             // based on the signature algorithm of the cert:
    252             //
    253             //    "SHA256withRSA" cert -> "SHA256withRSA" signature
    254             //    "SHA1withRSA" cert   -> "SHA1withRSA" signature
    255             //    "MD5withRSA" cert    -> "SHA1withRSA" signature (for backwards compatibility)
    256             //    any other cert       -> SignApk fails
    257             //
    258             // Here we ignore whatever the cert says, and instead use
    259             // whatever algorithm is used by the signature.
    260 
    261             String da = sigInfo.getDigestAlgorithm();
    262             String dea = sigInfo.getDigestEncryptionAlgorithm();
    263             String alg = null;
    264             if (da == null || dea == null) {
    265                 // fall back to the cert algorithm if the sig one
    266                 // doesn't look right.
    267                 alg = cert.getSigAlgName();
    268             } else {
    269                 alg = da + "with" + dea;
    270             }
    271             Signature sig = Signature.getInstance(alg);
    272             sig.initVerify(cert);
    273 
    274             // The signature covers all of the OTA package except the
    275             // archive comment and its 2-byte length.
    276             long toRead = fileLen - commentSize - 2;
    277             long soFar = 0;
    278             raf.seek(0);
    279             byte[] buffer = new byte[4096];
    280             boolean interrupted = false;
    281             while (soFar < toRead) {
    282                 interrupted = Thread.interrupted();
    283                 if (interrupted) break;
    284                 int size = buffer.length;
    285                 if (soFar + size > toRead) {
    286                     size = (int)(toRead - soFar);
    287                 }
    288                 int read = raf.read(buffer, 0, size);
    289                 sig.update(buffer, 0, read);
    290                 soFar += read;
    291 
    292                 if (listener != null) {
    293                     long now = System.currentTimeMillis();
    294                     int p = (int)(soFar * 100 / toRead);
    295                     if (p > lastPercent &&
    296                         now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
    297                         lastPercent = p;
    298                         lastPublishTime = now;
    299                         listener.onProgress(lastPercent);
    300                     }
    301                 }
    302             }
    303             if (listener != null) {
    304                 listener.onProgress(100);
    305             }
    306 
    307             if (interrupted) {
    308                 throw new SignatureException("verification was interrupted");
    309             }
    310 
    311             if (!sig.verify(sigInfo.getEncryptedDigest())) {
    312                 throw new SignatureException("signature digest verification failed");
    313             }
    314         } finally {
    315             raf.close();
    316         }
    317     }
    318 
    319     /**
    320      * Reboots the device in order to install the given update
    321      * package.
    322      * Requires the {@link android.Manifest.permission#REBOOT} permission.
    323      *
    324      * @param context      the Context to use
    325      * @param packageFile  the update package to install.  Must be on
    326      * a partition mountable by recovery.  (The set of partitions
    327      * known to recovery may vary from device to device.  Generally,
    328      * /cache and /data are safe.)
    329      *
    330      * @throws IOException  if writing the recovery command file
    331      * fails, or if the reboot itself fails.
    332      */
    333     public static void installPackage(Context context, File packageFile)
    334         throws IOException {
    335         String filename = packageFile.getCanonicalPath();
    336         Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
    337 
    338         final String filenameArg = "--update_package=" + filename;
    339         final String localeArg = "--locale=" + Locale.getDefault().toString();
    340         bootCommand(context, filenameArg, localeArg);
    341     }
    342 
    343     /**
    344      * Reboots the device and wipes the user data and cache
    345      * partitions.  This is sometimes called a "factory reset", which
    346      * is something of a misnomer because the system partition is not
    347      * restored to its factory state.  Requires the
    348      * {@link android.Manifest.permission#REBOOT} permission.
    349      *
    350      * @param context  the Context to use
    351      *
    352      * @throws IOException  if writing the recovery command file
    353      * fails, or if the reboot itself fails.
    354      * @throws SecurityException if the current user is not allowed to wipe data.
    355      */
    356     public static void rebootWipeUserData(Context context) throws IOException {
    357         rebootWipeUserData(context, false, context.getPackageName());
    358     }
    359 
    360     /** {@hide} */
    361     public static void rebootWipeUserData(Context context, String reason) throws IOException {
    362         rebootWipeUserData(context, false, reason);
    363     }
    364 
    365     /** {@hide} */
    366     public static void rebootWipeUserData(Context context, boolean shutdown)
    367             throws IOException {
    368         rebootWipeUserData(context, shutdown, context.getPackageName());
    369     }
    370 
    371     /**
    372      * Reboots the device and wipes the user data and cache
    373      * partitions.  This is sometimes called a "factory reset", which
    374      * is something of a misnomer because the system partition is not
    375      * restored to its factory state.  Requires the
    376      * {@link android.Manifest.permission#REBOOT} permission.
    377      *
    378      * @param context   the Context to use
    379      * @param shutdown  if true, the device will be powered down after
    380      *                  the wipe completes, rather than being rebooted
    381      *                  back to the regular system.
    382      *
    383      * @throws IOException  if writing the recovery command file
    384      * fails, or if the reboot itself fails.
    385      * @throws SecurityException if the current user is not allowed to wipe data.
    386      *
    387      * @hide
    388      */
    389     public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
    390             throws IOException {
    391         UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
    392         if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
    393             throw new SecurityException("Wiping data is not allowed for this user.");
    394         }
    395         final ConditionVariable condition = new ConditionVariable();
    396 
    397         Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
    398         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    399         context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
    400                 android.Manifest.permission.MASTER_CLEAR,
    401                 new BroadcastReceiver() {
    402                     @Override
    403                     public void onReceive(Context context, Intent intent) {
    404                         condition.open();
    405                     }
    406                 }, null, 0, null, null);
    407 
    408         // Block until the ordered broadcast has completed.
    409         condition.block();
    410 
    411         String shutdownArg = null;
    412         if (shutdown) {
    413             shutdownArg = "--shutdown_after";
    414         }
    415 
    416         String reasonArg = null;
    417         if (!TextUtils.isEmpty(reason)) {
    418             reasonArg = "--reason=" + sanitizeArg(reason);
    419         }
    420 
    421         final String localeArg = "--locale=" + Locale.getDefault().toString();
    422         bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
    423     }
    424 
    425     /**
    426      * Reboot into the recovery system to wipe the /cache partition.
    427      * @throws IOException if something goes wrong.
    428      */
    429     public static void rebootWipeCache(Context context) throws IOException {
    430         rebootWipeCache(context, context.getPackageName());
    431     }
    432 
    433     /** {@hide} */
    434     public static void rebootWipeCache(Context context, String reason) throws IOException {
    435         String reasonArg = null;
    436         if (!TextUtils.isEmpty(reason)) {
    437             reasonArg = "--reason=" + sanitizeArg(reason);
    438         }
    439 
    440         final String localeArg = "--locale=" + Locale.getDefault().toString();
    441         bootCommand(context, "--wipe_cache", reasonArg, localeArg);
    442     }
    443 
    444     /**
    445      * Reboot into the recovery system with the supplied argument.
    446      * @param args to pass to the recovery utility.
    447      * @throws IOException if something goes wrong.
    448      */
    449     private static void bootCommand(Context context, String... args) throws IOException {
    450         RECOVERY_DIR.mkdirs();  // In case we need it
    451         COMMAND_FILE.delete();  // In case it's not writable
    452         LOG_FILE.delete();
    453 
    454         FileWriter command = new FileWriter(COMMAND_FILE);
    455         try {
    456             for (String arg : args) {
    457                 if (!TextUtils.isEmpty(arg)) {
    458                     command.write(arg);
    459                     command.write("\n");
    460                 }
    461             }
    462         } finally {
    463             command.close();
    464         }
    465 
    466         // Having written the command file, go ahead and reboot
    467         PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    468         pm.reboot(PowerManager.REBOOT_RECOVERY);
    469 
    470         throw new IOException("Reboot failed (no permissions?)");
    471     }
    472 
    473     /**
    474      * Called after booting to process and remove recovery-related files.
    475      * @return the log file from recovery, or null if none was found.
    476      *
    477      * @hide
    478      */
    479     public static String handleAftermath() {
    480         // Record the tail of the LOG_FILE
    481         String log = null;
    482         try {
    483             log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
    484         } catch (FileNotFoundException e) {
    485             Log.i(TAG, "No recovery log file");
    486         } catch (IOException e) {
    487             Log.e(TAG, "Error reading recovery log", e);
    488         }
    489 
    490         // Delete everything in RECOVERY_DIR except those beginning
    491         // with LAST_PREFIX
    492         String[] names = RECOVERY_DIR.list();
    493         for (int i = 0; names != null && i < names.length; i++) {
    494             if (names[i].startsWith(LAST_PREFIX)) continue;
    495             File f = new File(RECOVERY_DIR, names[i]);
    496             if (!f.delete()) {
    497                 Log.e(TAG, "Can't delete: " + f);
    498             } else {
    499                 Log.i(TAG, "Deleted: " + f);
    500             }
    501         }
    502 
    503         return log;
    504     }
    505 
    506     /**
    507      * Internally, recovery treats each line of the command file as a separate
    508      * argv, so we only need to protect against newlines and nulls.
    509      */
    510     private static String sanitizeArg(String arg) {
    511         arg = arg.replace('\0', '?');
    512         arg = arg.replace('\n', '?');
    513         return arg;
    514     }
    515 
    516     private void RecoverySystem() { }  // Do not instantiate
    517 }
    518