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