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